jsonatapy 2.2.7__tar.gz → 2.2.8__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/CHANGELOG.md +866 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/Cargo.lock +28 -28
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/Cargo.toml +3 -3
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/PKG-INFO +44 -13
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/README.md +42 -11
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/bindings/c/examples/smoke.c +4 -1
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/pyproject.toml +2 -2
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/python/jsonatapy/__init__.py +14 -1
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/src/ast.rs +17 -4
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/src/ast_transform.rs +15 -5
- jsonatapy-2.2.8/src/builtins.rs +1747 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/src/capi.rs +8 -3
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/src/compiler.rs +1 -1
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/src/datetime.rs +86 -26
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/src/evaluator.rs +1513 -3255
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/src/functions.rs +339 -179
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/src/lib.rs +115 -18
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/src/parser.rs +181 -46
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/src/signature.rs +308 -22
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/src/value.rs +1 -1
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/src/vm.rs +32 -18
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/study/cli_fixtures.json +1 -1
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/tests/cli_test.rs +5 -1
- jsonatapy-2.2.8/tests/fixtures/builtin_arity.json +1 -0
- jsonatapy-2.2.8/tests/fixtures/builtin_differential.json +1 -0
- jsonatapy-2.2.8/tests/fixtures/builtin_signatures.json +65 -0
- jsonatapy-2.2.8/tests/fixtures/fastpath_differential.json +1 -0
- jsonatapy-2.2.8/tests/fixtures/fastpath_known_divergences.json +5 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/tests/integration_test.rs +1242 -23
- jsonatapy-2.2.7/CHANGELOG.md +0 -362
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/.gitignore +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/.gitmodules +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/.serena/.gitignore +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/.serena/memories/architecture.md +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/.serena/memories/conventions.md +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/.serena/memories/core.md +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/.serena/memories/memory_maintenance.md +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/.serena/memories/suggested_commands.md +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/.serena/memories/task_completion.md +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/.serena/memories/tech_stack.md +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/.serena/project.yml +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/LICENSE +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/benches/evaluator_bench.rs +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/bindings/c/README.md +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/bindings/c/jsonata.h +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/examples/evaluator_demo.rs +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/examples/host_functions.rs +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/examples/parser_demo.rs +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/examples/simd_json_bench.rs +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/python/jsonatapy/__main__.py +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/python/jsonatapy/_cli/__init__.py +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/python/jsonatapy/_cli/bindings.py +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/python/jsonatapy/_cli/error_format.py +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/python/jsonatapy/_cli/mcp_server.py +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/python/jsonatapy/_cli/resolve.py +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/python/jsonatapy/_cli/run.py +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/python/jsonatapy/py.typed +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/src/bin/jsonata/bindings.rs +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/src/bin/jsonata/error_format.rs +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/src/bin/jsonata/main.rs +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/src/bin/jsonata/resolve.rs +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/src/lazy.rs +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/src/parser/README.md +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/study/cli_fixtures_testdata.json +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/study/cli_spec.md +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/tests/cli_fixtures_test.rs +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/tests/datetime_picture_suite.rs +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/tests/host_functions_test.rs +0 -0
- {jsonatapy-2.2.7 → jsonatapy-2.2.8}/tests/parent_and_focus_binding_suite.rs +0 -0
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
### Deprecated
|
|
15
|
+
|
|
16
|
+
### Removed
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
### Security
|
|
21
|
+
|
|
22
|
+
## [2.2.8] "Conform-ata" - 2026-08-25
|
|
23
|
+
|
|
24
|
+
Primarily a conformance release, with documentation work on the Rust crate alongside it. Most
|
|
25
|
+
of what follows brings `jsonatapy` closer to the pinned jsonata-js (v2.2.2), and a fair amount
|
|
26
|
+
of it **changes the answer** for expressions that already run today. If you are upgrading, the
|
|
27
|
+
summary below is the part worth reading; the itemised entries follow.
|
|
28
|
+
|
|
29
|
+
### What changed, and who it affects
|
|
30
|
+
|
|
31
|
+
**Boolean coercion of containers — the broadest change.** JSONata has one truthiness rule and
|
|
32
|
+
applies it recursively: a container is truthy only if some element is truthy, all the way down.
|
|
33
|
+
`jsonatapy` had three rules, and the one every implicit consumer used asked only "is it
|
|
34
|
+
non-empty?". A single-element array holding a falsy value — `[0]`, `[""]`, `[false]`, `[null]` —
|
|
35
|
+
was therefore truthy where the reference says false. That affects `? :`, `and`, `or`, `$not`,
|
|
36
|
+
filter predicates, `$filter`, and any lambda whose result is coerced.
|
|
37
|
+
|
|
38
|
+
*You are exposed if* your data holds one-element arrays of falsy values (`{"flags": [false]}`,
|
|
39
|
+
`{"counts": [0]}`) or you build them (`[expr]`, `expr[]`, `$map`, `$filter`). Ordinary path
|
|
40
|
+
expressions are unaffected — a path matching one value unwraps to the scalar, so
|
|
41
|
+
`items[v=0].v` was already `0`, not `[0]`.
|
|
42
|
+
|
|
43
|
+
**`$formatNumber` produces different numbers.** Rounding was half-away-from-zero; the reference
|
|
44
|
+
rounds half-to-even. `$formatNumber(12.345, "#,##0.00")` was `"12.35"` and is now `"12.34"`.
|
|
45
|
+
Three further defects around empty fractional parts, optional digits and exponents are fixed in
|
|
46
|
+
the same pass.
|
|
47
|
+
|
|
48
|
+
*You are exposed if* you format numbers for display or comparison and depend on the previous
|
|
49
|
+
rounding.
|
|
50
|
+
|
|
51
|
+
**Sequence and null handling in paths.** `*` on a value with no children is `undefined` rather
|
|
52
|
+
than `null`; `*` and `#$i` now unwrap a lone result like every other sequence-producing step;
|
|
53
|
+
an explicit null is treated as a value by object construction and `[]` rather than
|
|
54
|
+
short-circuiting them; and `[]` and `[true]` are finally distinct operators.
|
|
55
|
+
|
|
56
|
+
*You are exposed if* you rely on the shape (wrapped vs unwrapped) of single-result expressions,
|
|
57
|
+
or on `null` where the reference produces `undefined` — the two differ in object construction,
|
|
58
|
+
which drops undefined-valued keys but keeps null-valued ones.
|
|
59
|
+
|
|
60
|
+
**Stricter argument validation.** Six builtins (`$base64encode`, `$base64decode`, `$toMillis`,
|
|
61
|
+
`$fromMillis`, `$formatInteger`, `$parseInteger`) had no signature at all and so validated
|
|
62
|
+
nothing. They now reject an explicit null with `T0410` where they previously returned `null`,
|
|
63
|
+
and they accept the context forms (`str.$base64encode()`) that never worked.
|
|
64
|
+
|
|
65
|
+
*You are exposed if* you pass a possibly-null value to any of those six and relied on getting
|
|
66
|
+
`null` back rather than an error.
|
|
67
|
+
|
|
68
|
+
**More permissive parsing in two places.** `$base64decode` now accepts what the reference
|
|
69
|
+
accepts — partial quanta, characters outside the alphabet, the URL-safe alphabet — instead of
|
|
70
|
+
raising. `$parseInteger` returns `NaN` for a value that does not match its picture rather than
|
|
71
|
+
raising `D3136`.
|
|
72
|
+
|
|
73
|
+
*You are exposed if* you depended on those raising to detect bad input.
|
|
74
|
+
|
|
75
|
+
**Error codes.** This is the largest single change by count. Errors across the evaluator and the
|
|
76
|
+
parser reported the wrong JSONata code, or none at all — an error with no code is not something
|
|
77
|
+
a caller can branch on. Of the reference suite's 273 code-bearing cases, 107 could not be
|
|
78
|
+
verified at 2.2.7 because our error carried no code to compare; that is now 2, and those two are
|
|
79
|
+
unreachable rather than unfixed. Parse errors in particular went from uncoded prose to carrying
|
|
80
|
+
`S0101`, `S0103`, `S0104`, `S0105`, `S0202`, `S0203`, `S0204`, `S0207`, `S0208`, `S0211`, `S0212`
|
|
81
|
+
and `S0401`, so a syntax error is now distinguishable from any other failure.
|
|
82
|
+
|
|
83
|
+
**Things that used to fail and now work.** Passing a builtin by reference through a variable
|
|
84
|
+
(`$f := $uppercase; $map(arr, $f)`) returned empty strings and now works, for every builtin.
|
|
85
|
+
`$eval` works as a callback. `$single` is recognised when passed by reference. Twenty-four
|
|
86
|
+
builtins that worked in direct calls but raised when passed to `$map`/`$filter` now behave
|
|
87
|
+
identically either way.
|
|
88
|
+
|
|
89
|
+
**Documentation — the Rust crate.** Its examples were marked `rust,ignore`, so nothing verified
|
|
90
|
+
they were correct; they are now four compiling doctests. Five rustdoc warnings that rendered as
|
|
91
|
+
broken links on docs.rs are gone, the crate header no longer names the wrong crate, and the
|
|
92
|
+
module list no longer advertises two private modules while omitting two feature-gated ones. On
|
|
93
|
+
the Python side, the package is classified Production/Stable rather than Beta — it still said
|
|
94
|
+
Beta at full reference-suite parity. Itemised under Added, Changed and Fixed below.
|
|
95
|
+
|
|
96
|
+
**Internals with no behavioural intent.** Builtin dispatch is now a single shared
|
|
97
|
+
implementation rather than three partial copies, and the differential harness gained the
|
|
98
|
+
ability to see distinctions it was previously blind to — `null` vs `undefined`, error codes,
|
|
99
|
+
and by-reference dispatch of evaluator-dependent builtins. Each of those blind spots was
|
|
100
|
+
hiding real divergences, which is where most of this release came from.
|
|
101
|
+
|
|
102
|
+
### Compatibility
|
|
103
|
+
|
|
104
|
+
The differential corpus — over 20,000 comparisons against jsonata-js across two engines and two
|
|
105
|
+
input paths — has **no known divergences** for the first time. The one deliberate exception is
|
|
106
|
+
base64's character set, documented below.
|
|
107
|
+
|
|
108
|
+
All 1686 reference-suite cases pass, and each now runs through **both** evaluation engines —
|
|
109
|
+
the suite previously ran only whichever the default resolved to, so the tree-walker was
|
|
110
|
+
exercised by the differential corpus and nowhere else.
|
|
111
|
+
|
|
112
|
+
Worth stating plainly rather than quoting the pass count: 43 of its error-expecting cases still
|
|
113
|
+
assert only that *something* was raised, because the errors we produce for them carry no JSONata
|
|
114
|
+
code to compare, and 15 more specify an error object that is not inspected. That is
|
|
115
|
+
[#144](https://github.com/txjmb/jsonata-core/issues/144), and it is mostly parser errors. It is
|
|
116
|
+
a gap in what the suite verifies, not a set of known failures — a test now pins the count so it
|
|
117
|
+
cannot grow unnoticed.
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
### Added
|
|
121
|
+
- The `jsonata-core` crate documentation now carries four compiling examples — quick start,
|
|
122
|
+
compile-once/evaluate-many, error handling, and host functions via `register_fn`. The
|
|
123
|
+
crate-level example was previously marked `rust,ignore`, so nothing verified it was correct;
|
|
124
|
+
`cargo test --doc` now runs 5 doctests.
|
|
125
|
+
|
|
126
|
+
### Changed
|
|
127
|
+
- The published Python package is now classified `Development Status :: 5 - Production/Stable`.
|
|
128
|
+
It still read `4 - Beta` at full reference-suite parity, and that classifier is the only
|
|
129
|
+
signal the PyPI sidebar and downstream trend trackers read.
|
|
130
|
+
- Every builtin that needs only its arguments is now implemented once, in `src/builtins.rs`,
|
|
131
|
+
and shared by the compiled path and the tree-walker instead of being written out in each.
|
|
132
|
+
Fifty-three builtins were spread across two dispatch sites: twenty-nine were implemented
|
|
133
|
+
twice, and twenty-four existed in exactly one, which is why `$type(x)` worked while
|
|
134
|
+
`$map(arr, $type)` raised. The differential corpus and the 1686-case reference suite
|
|
135
|
+
confirm the extraction preserves *value* and *error* behaviour for every builtin other
|
|
136
|
+
than the six listed under Fixed below. Both harnesses collapse `null` and `undefined` to
|
|
137
|
+
Python `None`, so that axis is confirmed only where the corpus's new object-construction
|
|
138
|
+
probes cover it directly (an undefined-valued key is dropped, a null-valued one is kept).
|
|
139
|
+
`$string()` against an explicit-null context is a further route disagreement at the
|
|
140
|
+
pre-branch baseline: the compiled path answered `undefined`, the tree-walker answered
|
|
141
|
+
`null`. The shared dispatcher answers `null` on both. `evaluate_function_call` drops from
|
|
142
|
+
2688 lines to 1266.
|
|
143
|
+
(issue [#107](https://github.com/txjmb/jsonata-core/issues/107))
|
|
144
|
+
|
|
145
|
+
### Deprecated
|
|
146
|
+
|
|
147
|
+
### Removed
|
|
148
|
+
|
|
149
|
+
### Fixed
|
|
150
|
+
- The last uncoded errors carry their code: calling a non-function is `T1006`, partially
|
|
151
|
+
applying one (a call carrying a `?` placeholder) is `T1008`, assigning to something that is
|
|
152
|
+
not a variable is `S0212`, and a type parameter applied to something other than a function or
|
|
153
|
+
array is `S0401`. Every reference case that names an error code now has that code compared
|
|
154
|
+
against ours, except two that cannot be reached at all — `$encodeUrl` on an unpaired
|
|
155
|
+
surrogate, where the expression never crosses into Rust to be parsed.
|
|
156
|
+
(issue [#144](https://github.com/txjmb/jsonata-core/issues/144))
|
|
157
|
+
- Parse errors carry their JSONata code. Unterminated strings are `S0101`, unsupported escapes
|
|
158
|
+
`S0103`, a malformed `\u` escape `S0104`, an unterminated backquoted name `S0105`, a wrong
|
|
159
|
+
token `S0202`, running out of input while expecting one `S0203`, an unknown operator `S0204`,
|
|
160
|
+
an unexpected end of expression `S0207`, and a symbol used where an operand belongs `S0211`.
|
|
161
|
+
Previously all of these were uncoded prose, so callers branching on error codes could not
|
|
162
|
+
distinguish a syntax error from any other failure.
|
|
163
|
+
(issue [#144](https://github.com/txjmb/jsonata-core/issues/144))
|
|
164
|
+
- Nine more evaluator errors carry their JSONata code: `$sqrt` of a negative number is `D3060`,
|
|
165
|
+
`$power` overflowing is `D3061`, the single-argument `$sort` on mixed types is `D3070`,
|
|
166
|
+
`$single` matching more than one value is `D3138`, a non-string object key is `T1003`, a
|
|
167
|
+
non-function right side of `~>` is `T2006`, and `$split` given a matcher function that does
|
|
168
|
+
not produce the expected structure is `T1010`. Calling a function without its `$` splits the
|
|
169
|
+
way jsonata-js splits it: a name that *is* a builtin gets `T1005` with the "did you mean
|
|
170
|
+
`$name`?" suggestion, anything else gets `T1006`.
|
|
171
|
+
(issue [#144](https://github.com/txjmb/jsonata-core/issues/144))
|
|
172
|
+
- `$decodeUrl` and `$decodeUrlComponent` report `D3140` on malformed input, naming the function
|
|
173
|
+
and quoting the value as jsonata-js does, instead of an uncoded "Invalid percent-encoded URL".
|
|
174
|
+
- An expression containing an unpaired surrogate no longer leaks a raw `UnicodeEncodeError`. A
|
|
175
|
+
Python `str` can hold one and a Rust `String` cannot, so such an expression cannot cross the
|
|
176
|
+
boundary to be parsed at all; PyO3's codec error now becomes a `ValueError` naming the
|
|
177
|
+
surrogate and its position, so the library keeps a single error type. (`$encodeUrl` on a lone
|
|
178
|
+
surrogate is `D3140` upstream; there is no point at which we could raise it, since the
|
|
179
|
+
expression never parses.)
|
|
180
|
+
- The reference suite compares the `code` of cases that specify an error *object*, not just
|
|
181
|
+
those with a bare `code` field. Nothing about those 15 was checked before.
|
|
182
|
+
- `$.7a` now raises `S0201`, not `S0213`. jsonata-js raises `S0213` ("literal value cannot be
|
|
183
|
+
used as a step") from a pass that runs *after* parsing, so an unexpected trailing token fails
|
|
184
|
+
the parse first; we raised it inline, before the trailing token was reached. Our `S0213` was
|
|
185
|
+
already right for `$.7` and `a.7` — only the ordering was wrong. A leftover token now also
|
|
186
|
+
carries `S0201`, the code the reference uses for it, instead of an uncoded "Expected end of
|
|
187
|
+
expression".
|
|
188
|
+
- Every reference-suite case now runs through both evaluation engines, and the suite pins how
|
|
189
|
+
many of its error cases it cannot actually verify. At 2.2.7 four cases had the two engines
|
|
190
|
+
raising different errors, each accepted because both were errors and the check was loose.
|
|
191
|
+
- The test reporter no longer crashes the run with an `INTERNALERROR` when a non-parametrized
|
|
192
|
+
test fails; it read `item.callspec` unguarded on the failure path.
|
|
193
|
+
- The reference-suite harness compares error codes it previously ignored. `extract_error_code`
|
|
194
|
+
was anchored to the start of the message, so any coded error carrying a prefix — `"Runtime
|
|
195
|
+
error: D3030: ..."`, `"Parse error: Invalid syntax: S0209: ..."` — read as *uncoded*, and an
|
|
196
|
+
uncoded error is accepted for any expected code. 36 cases that emit exactly the right code
|
|
197
|
+
were passing without it ever being compared, and one emitting the wrong code passed the same
|
|
198
|
+
way. Now 228 of the 273 code-expecting cases are genuinely compared, up from 191.
|
|
199
|
+
(issue [#144](https://github.com/txjmb/jsonata-core/issues/144))
|
|
200
|
+
- Five rustdoc warnings that rendered as broken links on docs.rs are gone. JSONata syntax in
|
|
201
|
+
doc comments — `[expr]`, `|location|update[,delete]|` — was being parsed as intra-doc links,
|
|
202
|
+
and `Rc<str>` as an unclosed HTML tag. `cargo doc` is now warning-free under both the default
|
|
203
|
+
features and `--all-features`.
|
|
204
|
+
- The crate's documentation header said `jsonatapy` (the crate is `jsonata-core`), and its
|
|
205
|
+
module list advertised `datetime` and `signature` as public when both are private while
|
|
206
|
+
omitting the feature-gated `lazy` and `capi`.
|
|
207
|
+
- A positional predicate that names the same index more than once now repeats the element, as
|
|
208
|
+
jsonata-js does: `nums[[0,0]]` is `[10, 10]`, not `10`. The reference walks the selector array
|
|
209
|
+
and pushes the item on every hit; we used an `any()`-style membership test, which can only ever
|
|
210
|
+
yield each element once. Singleton sequences repeat too — `num[[0,0]]` is `[5, 5]`.
|
|
211
|
+
- The tree-walker now recognises an *array* of indices as a positional selector against a scalar.
|
|
212
|
+
It tested only for a single number, so `num[[0]]` was `undefined` and `num[[1]]` was `5` — both
|
|
213
|
+
inverted, because the array fell through to the truthiness branch where an all-falsy container
|
|
214
|
+
is falsy and a non-empty one is truthy. The compiled path was already correct, so the two
|
|
215
|
+
engines disagreed.
|
|
216
|
+
- `$eval` can be passed by reference: `$map(["1+1"], $eval)` is `2` rather than an error. It is
|
|
217
|
+
the one evaluator-dependent builtin whose arguments are ordinary values — an expression string
|
|
218
|
+
and an optional focus — so unlike the other nine it runs from evaluated values. Its
|
|
219
|
+
implementation is now shared by the two dispatch paths rather than living only in
|
|
220
|
+
`evaluate_function_call`.
|
|
221
|
+
- The other nine evaluator-dependent builtins (`$map`, `$filter`, `$reduce`, `$single`, `$sift`,
|
|
222
|
+
`$each`, `$sort`, `$match`, `$replace`) now raise `T0410` when handed to a higher-order
|
|
223
|
+
function, matching jsonata-js, instead of an uncoded internal error. They still cannot run
|
|
224
|
+
as callbacks — each needs a *function* argument and a callback receives a value and an index,
|
|
225
|
+
which the reference rejects on the signature too — so this is the error code, not the
|
|
226
|
+
behaviour.
|
|
227
|
+
- `$single` is recognised as a builtin when passed by reference. The internal name list carried
|
|
228
|
+
`singletonArray`, which is not a JSONata function and appears nowhere in jsonata-js, and
|
|
229
|
+
omitted `single` — so `$map(arr, $single)` raised "Argument 2 must be Function" while direct
|
|
230
|
+
calls worked. (issue [#140](https://github.com/txjmb/jsonata-core/issues/140))
|
|
231
|
+
- `$eval(null)` now raises `T0410` rather than returning `null`. The reference's signature is
|
|
232
|
+
`<sx?:x>`, and `s` admits *missing* but not null; `$eval(nothing)` still propagates undefined.
|
|
233
|
+
- Truthiness is now one rule, applied recursively, matching `$boolean`. JSONata coerces a
|
|
234
|
+
value to boolean the same way everywhere: a container is truthy only if some element is
|
|
235
|
+
truthy, checked all the way down, so `[0]`, `[[0]]` and `[0,0]` are falsy. `jsonata-core`
|
|
236
|
+
had three rules — `$boolean` (correct), a flat "is it non-empty?" used by every implicit
|
|
237
|
+
consumer, and a third half-recursive one used only by `?:`. `$not([0])` was `false` where
|
|
238
|
+
`$boolean([0])` was already `false`, so `$not` was not computing `!$boolean(x)`. This
|
|
239
|
+
changes the answer for `$not`, `? :`, `and`, `or`, filter predicates, `$filter` and any
|
|
240
|
+
lambda whose result is coerced — 54 measured cases across those seven consumers — whenever
|
|
241
|
+
the value is a container whose contents are all falsy. The `?:`-only rule is deleted rather
|
|
242
|
+
than realigned: with `is_truthy` correct the two agree exactly.
|
|
243
|
+
(issue [#111](https://github.com/txjmb/jsonata-core/issues/111))
|
|
244
|
+
- `$formatNumber` now produces the same value as jsonata-js. Four separate defects, all in
|
|
245
|
+
picture analysis and formatting:
|
|
246
|
+
- **Rounding was half-away-from-zero, not half-to-even.** The reference routes this through
|
|
247
|
+
the same helper `$round` uses. `$formatNumber(12.345, "#,##0.00")` was `"12.35"` and is
|
|
248
|
+
`"12.34"`; `$formatNumber(0.25, "0.0")` was `"0.3"` and is `"0.2"`. Our `$round` builtin
|
|
249
|
+
was already correct — `$formatNumber` simply did not use it. This produced wrong numbers
|
|
250
|
+
for ordinary pictures, not just edge cases.
|
|
251
|
+
- **An empty fractional part invented a digit and a separator.** Fractional digit counts now
|
|
252
|
+
come only from the picture, so `"0."` has none: `$formatNumber(1.5, "0.")` was `"1.5"` and
|
|
253
|
+
is `"2"`, and `$formatNumber(1, "0.")` was `"1."` and is `"1"`.
|
|
254
|
+
- **`#` did not suppress a leading zero.** `$formatNumber(0.25, "#.#")` was `"0.3"` and is
|
|
255
|
+
`".2"`, per F&O 4.7.4's adjustments to the minimum digit counts.
|
|
256
|
+
- **The exponent's upper bound was inclusive.** `$formatNumber(1, "#.e0")` was `"0.1e1"` and
|
|
257
|
+
is `"1.0e0"`.
|
|
258
|
+
|
|
259
|
+
326 cases across 21 pictures now agree, including negatives, grouping, sub-pictures and
|
|
260
|
+
percent/per-mille. Values at or beyond `1e21` are deliberately excluded: JavaScript switches
|
|
261
|
+
to exponential notation there and the reference formats the resulting text as if it were
|
|
262
|
+
digits, so `$formatNumber(1e21, "#,##0.00")` is `"1e,+21.00"` upstream — a grouping separator
|
|
263
|
+
inside the exponent. (issue [#136](https://github.com/txjmb/jsonata-core/issues/136))
|
|
264
|
+
- `$formatNumber` now reports the same picture-string error as jsonata-js. The reference runs
|
|
265
|
+
every sub-picture check in sequence and lets the *last* failure name the error (F&O 4.7.3);
|
|
266
|
+
ours returned on the first, so a picture failing several checks reported the wrong one —
|
|
267
|
+
`"k"` fails both `D3085` and `D3086` and is `D3086`, `"%%"` was `D3082`, `".."` was `D3081`
|
|
268
|
+
and `"e"` was `D3085`. A picture containing no active character at all was also swallowed
|
|
269
|
+
whole as a prefix rather than treated as the active part, which is what stopped `D3086` from
|
|
270
|
+
ever firing for `"k"` or `"%%"`. 29 pictures now agree where 7 diverged.
|
|
271
|
+
(issue [#135](https://github.com/txjmb/jsonata-core/issues/135))
|
|
272
|
+
- Error *codes* now match jsonata-js where they did not. The differential harness compared
|
|
273
|
+
only "did it raise", so any error satisfied an expected error — `$replace(1)` raising
|
|
274
|
+
"Argument count mismatch" read as a match for the reference's `T0410`. Comparing codes
|
|
275
|
+
surfaced 112 cases across nine shapes, all now fixed: signature arity failures carry `T0410`;
|
|
276
|
+
`$single` reports `D3138`/`D3139`; `$power` with a missing exponent reports `D3061`;
|
|
277
|
+
`$toMillis` on an unparseable timestamp reports `D3110`; `$millis`/`$now` handed arguments
|
|
278
|
+
by a higher-order function report `T0410`; and `$error`/`$assert` treat an undefined argument
|
|
279
|
+
as the signature says they must — `s`/`b` admit *missing*, so `$error(missing.x)` is
|
|
280
|
+
`D3137` ("no message") and `$assert(missing.x)` is `D3141` (a failed assertion), not type
|
|
281
|
+
errors. (issue [#126](https://github.com/txjmb/jsonata-core/issues/126))
|
|
282
|
+
- A builtin bound to a variable now works as a callback: `($f := $uppercase; $map(arr, $f))`
|
|
283
|
+
is `["A", "B"]` rather than `["", ""]`. Binding stores a `JValue::Builtin`, which is not a
|
|
284
|
+
lambda, so `apply_function` fell through to evaluating `$f` as an ordinary variable and the
|
|
285
|
+
callback yielded the function value instead of calling it. Callback arity was resolved from
|
|
286
|
+
the variable's name too, so `$map` handed a one-argument builtin all three of its arguments.
|
|
287
|
+
Both now resolve through the bound builtin, and a host-registered override of the same name
|
|
288
|
+
still suppresses arity truncation. Every builtin was affected, not just the reported
|
|
289
|
+
`$uppercase`; direct invocation (`$f("a")`) always worked, which is why this looked narrow.
|
|
290
|
+
(issue [#126](https://github.com/txjmb/jsonata-core/issues/126))
|
|
291
|
+
- `$base64decode` now accepts what jsonata-js accepts. The reference decodes through Node's
|
|
292
|
+
`Buffer`, which ignores characters outside the base64 alphabet, stops at the first padding
|
|
293
|
+
character, takes the URL-safe alphabet alongside the standard one, and drops an incomplete
|
|
294
|
+
trailing quantum — so `$base64decode("a")` is `""`, `$base64decode("YQ")` is `"a"` and
|
|
295
|
+
`$base64decode("!!!!")` is `""`, where a strict decoder rejects all three.
|
|
296
|
+
- `$parseInteger` no longer raises `D3136` on a value that does not match its picture. The
|
|
297
|
+
reference runs the picture parse without validating the input — its own source marks that
|
|
298
|
+
path `TODO validate input based on the matcher regex` — so the answer is JavaScript's
|
|
299
|
+
`parseInt`: a leading run of digits, or `NaN` when there is none. `$parseInteger("12a", "000")`
|
|
300
|
+
is `12` and `$parseInteger("abc", "000")` is `NaN`. A malformed *picture* still raises
|
|
301
|
+
`D3130`, and the letters/roman/words parsers are untouched because date-time component
|
|
302
|
+
parsing shares them and needs the error. (issue [#126](https://github.com/txjmb/jsonata-core/issues/126))
|
|
303
|
+
|
|
304
|
+
- An explicit null now behaves as a value in object construction and in the `[]` array-keep,
|
|
305
|
+
rather than short-circuiting them: `nul{"a": $}` is `{"a": null}` (it was `null`, and
|
|
306
|
+
disagreed with the dotted `nul.{"a": $}`, which was already right), `nul[]` is `[null]`,
|
|
307
|
+
and `nul[][0]` is `[null]`. Four separate sites each carried a pre-`Undefined` `JValue::Null`
|
|
308
|
+
arm that made the operation a no-op. `emptyarr[]` is now `[]` rather than `undefined` — an
|
|
309
|
+
empty array held in a field is a value, unlike an empty result sequence such as `arr.p` over
|
|
310
|
+
`{"arr": []}`, which stays `undefined`.
|
|
311
|
+
- `[]` and `[true]` are no longer the same thing. Both parsed to `Predicate(Boolean(true))`,
|
|
312
|
+
but jsonata treats them as different operators: `[]` keeps the result an array while
|
|
313
|
+
`[true]` is an ordinary filter that keeps everything and then unwraps a lone result. One of
|
|
314
|
+
the two therefore had to be wrong for every input — `num[]` and `num[true]` were both `[5]`
|
|
315
|
+
where the reference gives `[5]` and `5`. A dedicated `AstNode::KeepArray`/`Stage::KeepArray`
|
|
316
|
+
marker separates them. (issue [#126](https://github.com/txjmb/jsonata-core/issues/126))
|
|
317
|
+
- `#$i` positional binding now unwraps a one-value result, matching every other
|
|
318
|
+
sequence-producing step: `num#$i` is `5` rather than `[5]`, and `deep#$i.$i` is `0` rather
|
|
319
|
+
than `[0]`. The rule holds for every input kind — `arr#$i` and `arrobj#$i` only ever looked
|
|
320
|
+
correct because a multi-element result has no singleton to unwrap, and the filtered form
|
|
321
|
+
`arr#$i[$i=0]` was right for the unrelated reason that a filter predicate already forced
|
|
322
|
+
the unwrap. (issue [#126](https://github.com/txjmb/jsonata-core/issues/126))
|
|
323
|
+
- `*` applied to anything without children is now `undefined` rather than `null`, and its
|
|
324
|
+
result unwraps a lone value the way every other sequence-producing step does. jsonata-js
|
|
325
|
+
guards the wildcard with `typeof input === 'object' && input !== null`, so `num.*`,
|
|
326
|
+
`str.*`, `true.*` and the rest all map over nothing; only the `null` case had been fixed
|
|
327
|
+
here, leaving the arm inconsistent with the catch-all beside it, and the two collapse into
|
|
328
|
+
one now that they give the same answer. Separately, `*` was missing from the list of steps
|
|
329
|
+
that produce a query-result sequence — the list `**` and filter predicates are already on —
|
|
330
|
+
so a one-value result stayed wrapped: `deep.*` over `{"a": {"b": 1}}` was `[{"b": 1}]` and
|
|
331
|
+
is now `{"b": 1}`, which also lets `deep.*.*` reach the inner value instead of an array.
|
|
332
|
+
(issue [#126](https://github.com/txjmb/jsonata-core/issues/126))
|
|
333
|
+
- Six builtins — `$base64encode`, `$base64decode`, `$toMillis`, `$fromMillis`,
|
|
334
|
+
`$formatInteger` and `$parseInteger` — now validate their arguments. They had no entry in
|
|
335
|
+
the builtin signature table, and a missing entry is not a weaker check but no check at
|
|
336
|
+
all: validation returns early for a name it does not know, so these six fell back to
|
|
337
|
+
hand-rolled arity and type guards. An explicit null returned `null` where jsonata-js
|
|
338
|
+
raises `T0410` (`$base64encode(null)`), the `-` context marker never fired
|
|
339
|
+
(`str.$base64encode()` was `null`, not `"YQ=="`), `$formatInteger`'s context form was
|
|
340
|
+
rejected as an arity error, and `$base64encode`/`$base64decode` raised a type error on a
|
|
341
|
+
missing argument instead of propagating `undefined`. The table had drifted to a strict
|
|
342
|
+
*subset* of jsonata-js's own — 55 of its 63 entries, every one byte-identical, with eight
|
|
343
|
+
simply absent — which is why nothing caught it; a new test compares the two tables in
|
|
344
|
+
both directions against a generated fixture, so a submodule bump that adds or changes a
|
|
345
|
+
signature now fails CI. The two entries still absent are deliberate and named in that
|
|
346
|
+
test: `$clone` is not implemented here at all, and `$eval` needs the evaluator.
|
|
347
|
+
(issue [#126](https://github.com/txjmb/jsonata-core/issues/126))
|
|
348
|
+
- `$sum`, `$max`, `$min` and `$average` over a path of the form `array.field` no longer
|
|
349
|
+
silently skip non-numeric values. The fused aggregate fast path
|
|
350
|
+
(`Evaluator::try_fused_aggregate`) reimplemented aggregate semantics rather than
|
|
351
|
+
delegating to them, and treated a present non-numeric field the same as an absent one.
|
|
352
|
+
With `{"orders": [{"p": 1}, {"p": "free"}]}`, `$sum(orders.p)` returned `1` instead of
|
|
353
|
+
raising `T0412` — a plausible but wrong number rather than an error. The same path also
|
|
354
|
+
returned `0`/`null` for an empty sequence where jsonata-js returns `undefined`. The fast
|
|
355
|
+
path now declines when its assumptions do not hold, so the canonical aggregate produces
|
|
356
|
+
both the error and the empty-sequence semantics. (issue [#97](https://github.com/txjmb/jsonata-core/issues/97))
|
|
357
|
+
- Builtin argument handling now matches jsonata-js for a missing argument in a *required*
|
|
358
|
+
slot. The reference validates a call and then hands the arguments to the function body
|
|
359
|
+
unchanged, so an undefined that the signature admitted reaches a JavaScript expression
|
|
360
|
+
and JavaScript's coercion supplies the answer: `$substring("abcdef", missing.x)` is
|
|
361
|
+
`"abcdef"` (and `$substring("abcdef", missing.x, 2)` is `""`, because the undefined start
|
|
362
|
+
makes the end `NaN`), `$pad("a", missing.x)` is `"a"`, and `$substringBefore` and
|
|
363
|
+
`$lookup` stringify the missing argument to the literal `"undefined"` rather than
|
|
364
|
+
treating it as absent. `$trim(missing.x)` propagates undefined instead of raising.
|
|
365
|
+
- `$substring`, `$substringBefore` and `$substringAfter` given a single missing argument
|
|
366
|
+
now raise `T0411` on both engines. The compiled path applied its undefined-propagation
|
|
367
|
+
shortcut before signature validation; jsonata-js validates first, and for these three
|
|
368
|
+
the lone undefined argument binds to parameter 2 while parameter 1 comes from the
|
|
369
|
+
context. The tree-walker never had the shortcut, so the two engines disagreed.
|
|
370
|
+
- `$spread` and `$each` now follow jsonata-js's sequence rules: `$spread({"k": 1})` is the
|
|
371
|
+
object rather than `[{"k": 1}]`, and `$spread([])`, `$spread({})` and `$each({}, fn)` are
|
|
372
|
+
`undefined` rather than empty containers. `$spread`'s *array* branch is deliberately
|
|
373
|
+
exempt — the reference folds it with `concat`, which drops the sequence flag — so
|
|
374
|
+
`$spread([{"k": 1}])` stays wrapped. `$each` also no longer drops explicit nulls from its
|
|
375
|
+
results.
|
|
376
|
+
- `$sift(obj)` now raises instead of returning `undefined`. The one-argument form is
|
|
377
|
+
`$sift(function)`, with the object taken from the context.
|
|
378
|
+
(issue [#104](https://github.com/txjmb/jsonata-core/issues/104))
|
|
379
|
+
- `$trim`, `$merge`, `$reverse`, `$distinct`, `$join` and `$keys` given a missing argument
|
|
380
|
+
now yield `undefined` rather than `null`, matching jsonata-js. This was visible through
|
|
381
|
+
object construction, which drops an undefined-valued key but keeps a null-valued one:
|
|
382
|
+
`{"k": $trim(missing)}` was `{"k": null}` and is now `{}`. For the first five, the two
|
|
383
|
+
evaluation routes previously disagreed — the compiled/VM path returned `null` while the
|
|
384
|
+
tree-walker returned `undefined` — and the shared dispatcher makes them agree. `$keys`
|
|
385
|
+
was wrong on both routes; the tree-walker arm had no `Undefined` case at all, and the new
|
|
386
|
+
shared dispatcher's corpus is what exposed it.
|
|
387
|
+
(issue [#107](https://github.com/txjmb/jsonata-core/issues/107))
|
|
388
|
+
|
|
389
|
+
### Deliberately not changed
|
|
390
|
+
- `$base64encode`/`$base64decode` keep treating their payload as UTF-8, because jsonata's own
|
|
391
|
+
documentation asks for both latin1 *and* UTF-8 and its implementation matches only the first.
|
|
392
|
+
`$base64encode` is documented as latin1 — "all characters in the string are in the 0x00 to
|
|
393
|
+
0xFF range... Unicode characters outside of that range are not supported" — while
|
|
394
|
+
`$base64decode` is documented as "using a UTF-8 Unicode codepage", which its implementation
|
|
395
|
+
does not do. No reading of the reference is self-consistent, and UTF-8 on both sides is the
|
|
396
|
+
half that matches a documented contract exactly while also round-tripping. Above `0xFF`
|
|
397
|
+
nothing is defined at all: `window.btoa` throws `InvalidCharacterError` where Node truncates
|
|
398
|
+
each UTF-16 code unit to a byte, so browser and Node disagree. The cost is real but narrow —
|
|
399
|
+
for `0x80`–`0xFF`, encode has an environment-independent documented answer we do not give
|
|
400
|
+
(`$base64encode("héllo")` is `"aOlsbG8="` upstream, `"aMOpbGxv"` here); matching it would
|
|
401
|
+
require latin1 decode too, which would then contradict the decode docs. A unit test pins our
|
|
402
|
+
choice, with the full reasoning, so it cannot flip unnoticed.
|
|
403
|
+
(issue [#126](https://github.com/txjmb/jsonata-core/issues/126))
|
|
404
|
+
|
|
405
|
+
### Security
|
|
406
|
+
|
|
407
|
+
## [2.2.7] - 2026-07-21
|
|
408
|
+
|
|
409
|
+
### Added
|
|
410
|
+
|
|
411
|
+
### Changed
|
|
412
|
+
- Dependabot no longer auto-bumps the `tests/jsonata-js` reference submodule (removed the
|
|
413
|
+
`gitsubmodule` ecosystem entry). Reference-suite updates are handled by the
|
|
414
|
+
`sync-jsonata.yml` workflow, which runs the conformance suite against each new jsonata-js
|
|
415
|
+
release and opens a tracking issue (or a clean PR) — avoiding context-free failing bump
|
|
416
|
+
PRs like #82.
|
|
417
|
+
|
|
418
|
+
### Deprecated
|
|
419
|
+
|
|
420
|
+
### Removed
|
|
421
|
+
|
|
422
|
+
### Fixed
|
|
423
|
+
- Ensures compliance with the **jsonata-js 2.2.2** reference test suite (reference submodule
|
|
424
|
+
bumped to `6c7e95f`); the full reference suite — 1686 cases — passes. Three behavior
|
|
425
|
+
changes were required to match jsonata-js 2.2.2:
|
|
426
|
+
- `$contains(str, token)` now returns `undefined` when either argument is undefined,
|
|
427
|
+
instead of raising a type error (jsonata-js #809).
|
|
428
|
+
- `$each(obj, fn)` now returns `undefined` when its first argument is undefined, instead
|
|
429
|
+
of raising `each() first argument must be an object`.
|
|
430
|
+
- An object constructor (group-by) applied to an empty or undefined sequence now yields
|
|
431
|
+
an empty object `{}` instead of `undefined` (jsonata-js #817, "correctly handle empty
|
|
432
|
+
joins"); `null` input still returns `null` and non-empty grouping is unchanged.
|
|
433
|
+
|
|
434
|
+
### Security
|
|
435
|
+
|
|
436
|
+
## [2.2.6] - 2026-07-20
|
|
437
|
+
|
|
438
|
+
### Added
|
|
439
|
+
- Host-callable custom functions (Rust core): `Evaluator::register_fn` and
|
|
440
|
+
`Evaluator::register_fn_override` let a host register native functions callable
|
|
441
|
+
from an expression as `$name(...)` — the equivalent of jsonata-js's
|
|
442
|
+
`registerFunction`. Functions are plain closures
|
|
443
|
+
(`Fn(&[JValue]) -> Result<JValue, EvaluatorError>`) and resolve after the
|
|
444
|
+
expression's own bindings/lambdas and before built-ins. `register_fn` rejects
|
|
445
|
+
collisions with built-ins; `register_fn_override` allows deliberately replacing
|
|
446
|
+
the impure built-ins (`$now`, `$millis`, `$random`, `$eval`) for determinism
|
|
447
|
+
injection or sandboxing. Evaluation stays synchronous. See
|
|
448
|
+
`examples/host_functions.rs` and the Rust crate docs.
|
|
449
|
+
- Host-callable custom functions (Python binding): `JsonataExpression.register(name,
|
|
450
|
+
func)` and `.register_override(name, func)` expose the above to Python. The callable
|
|
451
|
+
receives already-evaluated positional arguments and must return a JSON-compatible
|
|
452
|
+
value synchronously; an `async def` (coroutine) is rejected at call time with
|
|
453
|
+
guidance to await I/O outside jsonata and pass results via `bindings`. Collision and
|
|
454
|
+
compilable-builtin-override rules are validated at `register()` time.
|
|
455
|
+
- Host-callable custom functions (C ABI): `jsonata_register_fn(expr, name, fn, user_data)`
|
|
456
|
+
and `jsonata_register_fn_override(...)` expose the feature to C and any language with C
|
|
457
|
+
interop. The callback receives its arguments as a JSON array string and returns a JSON
|
|
458
|
+
result string (jsonata copies it; the host retains ownership), or NULL to signal an error.
|
|
459
|
+
See `bindings/c/jsonata.h`, `bindings/c/README.md`, and `bindings/c/examples/smoke.c`.
|
|
460
|
+
|
|
461
|
+
### Changed
|
|
462
|
+
|
|
463
|
+
### Deprecated
|
|
464
|
+
|
|
465
|
+
### Removed
|
|
466
|
+
|
|
467
|
+
### Fixed
|
|
468
|
+
- Path expressions no longer drop explicit `null` values from query-result sequences.
|
|
469
|
+
`evaluate_path`'s array-mapping fast path predates the null/undefined migration in #32
|
|
470
|
+
and skipped both, so `arr.p` over `[{"p": 1}, {"p": null}]` returned `1` instead of
|
|
471
|
+
`[1, null]`. Only an *absent* field is undefined and drops out; a present `null` is a
|
|
472
|
+
value and stays. Fixed for both the JSON and Python-dict (lazy view) routes. This
|
|
473
|
+
corrects everything downstream of such a sequence — `$count`, array construction,
|
|
474
|
+
comparison and arithmetic operands, and the fused aggregates, which now raise `T0412`
|
|
475
|
+
on a null element rather than silently summing around it.
|
|
476
|
+
([#98](https://github.com/txjmb/jsonata-core/issues/98), root cause 1)
|
|
477
|
+
- Filter predicates now unwrap a single-element result, so `arr[p = 1]` is `{"p": 1}` rather
|
|
478
|
+
than `[{"p": 1}]`. The tree-walker decided this from `step.stages` alone, but `arr[p = 1]`
|
|
479
|
+
parses its predicate as a `Predicate` step *node* with empty stages, so no filter written
|
|
480
|
+
that way was ever recognised as an array operation. Numeric-literal predicates are excluded:
|
|
481
|
+
those are index access and already return the selected element, so counting them would
|
|
482
|
+
unwrap twice and turn `a[0]` over `[[5]]` into `5`.
|
|
483
|
+
([#98](https://github.com/txjmb/jsonata-core/issues/98), root cause 2)
|
|
484
|
+
- Numeric filter predicates now select by position instead of being treated as truthy. In
|
|
485
|
+
JSONata `arr[p]` keeps an element only when `p` equals that element's own index; negative
|
|
486
|
+
values count from the end and fractional values floor, and an array of numbers is a set of
|
|
487
|
+
such indices. The tree-walker previously evaluated the predicate against the whole array
|
|
488
|
+
and treated a numeric result as a multi-index selector, so `arr[p]` over
|
|
489
|
+
`[{"p": 1}, {"p": 2}]` returned both elements instead of nothing. Fixed for standalone
|
|
490
|
+
predicates; filters in *stage* position (`a.b[-1]`, which maps the index over each
|
|
491
|
+
extracted sub-array) keep their existing semantics.
|
|
492
|
+
([#98](https://github.com/txjmb/jsonata-core/issues/98), root cause 3)
|
|
493
|
+
- The compiled path and bytecode VM now apply the same index rule. `CompiledStep` records
|
|
494
|
+
whether its filter came from a standalone `Predicate` step or a `Stage::Filter`, a
|
|
495
|
+
distinction the compiler previously discarded on the stated assumption that "both
|
|
496
|
+
encodings have identical runtime semantics" -- true for boolean predicates, false for
|
|
497
|
+
numeric ones. ([#98](https://github.com/txjmb/jsonata-core/issues/98), root cause 3)
|
|
498
|
+
- A predicate applied to a non-array value now treats it as the singleton sequence it is:
|
|
499
|
+
index 0, length 1. `arr[p]` over `{"p": 1}` is undefined (1 does not match index 0) and
|
|
500
|
+
`arr[-1]` wraps to the value itself. A string predicate on an object is no longer computed
|
|
501
|
+
property access -- `o["a"]` keeps the object because a non-empty string is truthy, matching
|
|
502
|
+
jsonata-js, rather than looking up the key.
|
|
503
|
+
([#98](https://github.com/txjmb/jsonata-core/issues/98), root cause 4)
|
|
504
|
+
- A non-empty Python `dict` is no longer falsy on the bytecode VM and compiled paths.
|
|
505
|
+
Dicts cross the boundary as a lazy view rather than a materialised object, and
|
|
506
|
+
`compiled_is_truthy` had no arm for that variant, so it fell through to its catch-all and
|
|
507
|
+
returned `false` for every one. This affected any truthiness context on the compiled path
|
|
508
|
+
-- `o ? a : b`, `and`/`or`, `$boolean`, `$not`, filter predicates -- and only when data was
|
|
509
|
+
passed as a dict, so the same expression over an equivalent JSON string was correct. The
|
|
510
|
+
tree-walker was unaffected. ([#98](https://github.com/txjmb/jsonata-core/issues/98))
|
|
511
|
+
- Ordered comparisons (`<`, `<=`, `>`, `>=`) against an undefined operand now return undefined
|
|
512
|
+
instead of raising `T2010`, and an explicit `null` operand now raises `T2010` instead of
|
|
513
|
+
returning null. `ordered_compare` predated the null/undefined split and matched only on
|
|
514
|
+
`JValue::Null`, so a real `Undefined` reached its catch-all. Rewritten to jsonata-js's rule:
|
|
515
|
+
only numbers, strings and undefined are comparable; an undefined operand yields undefined;
|
|
516
|
+
otherwise a type mismatch is `T2009`.
|
|
517
|
+
- An unbound variable (`$x`) now evaluates to undefined rather than null, so `3 > $x` is
|
|
518
|
+
undefined, `{"a": $x}` drops the key, and `$not($x)` is undefined -- all matching jsonata-js.
|
|
519
|
+
The surrounding comment already described these as the intended results; only the value was
|
|
520
|
+
wrong. ([#98](https://github.com/txjmb/jsonata-core/issues/98), root cause 5)
|
|
521
|
+
- Explicit nulls now survive a stage filter (`arr.p[-1]`, `arr.p[0]`, `arr.p[]`). The
|
|
522
|
+
tuple/stage branch of `evaluate_path` mapped an absent field to `JValue::Null` and then
|
|
523
|
+
skipped every null, dropping present nulls alongside genuinely missing fields -- the same
|
|
524
|
+
pre-migration pattern already fixed in the no-stages fast path, in three more places
|
|
525
|
+
(the object arm, the tuple arm and the lazy-dict arm).
|
|
526
|
+
([#98](https://github.com/txjmb/jsonata-core/issues/98))
|
|
527
|
+
- `arr.p[-1]` now takes the last element of each extracted group on the bytecode VM, matching
|
|
528
|
+
the tree-walker. Numeric-literal predicates are index access and are deliberately left to
|
|
529
|
+
the tree-walker, but the guard tested only for `AstNode::Number` -- `[-1]` parses as a
|
|
530
|
+
*negation* of a literal, slipped through, and compiled to a plain truthy constant that kept
|
|
531
|
+
every element. ([#98](https://github.com/txjmb/jsonata-core/issues/98))
|
|
532
|
+
- Arithmetic on an explicit `null` now raises instead of silently producing null, including
|
|
533
|
+
when the null arrives at runtime rather than as a literal -- `$map([1, null], function($v)
|
|
534
|
+
{ $v * 2 })` raises `T2001` where it previously returned `[2, null]`. Only *undefined*
|
|
535
|
+
propagates. Error codes now match jsonata-js: a bad left operand is `T2001` and a bad right
|
|
536
|
+
operand is `T2002` (previously `T2002` for both), and each defined operand is type-checked
|
|
537
|
+
before undefined propagation, so `false + $x` raises rather than returning undefined. The
|
|
538
|
+
five tree-walker operators now delegate to the same shared implementation as the compiled
|
|
539
|
+
path and VM instead of each carrying its own copy of the null handling.
|
|
540
|
+
([#98](https://github.com/txjmb/jsonata-core/issues/98))
|
|
541
|
+
- Ordered comparisons inside filters and sort comparators now reject uncomparable operands.
|
|
542
|
+
`compiled_ordered_cmp` was the un-migrated twin of `Evaluator::ordered_compare`: it still
|
|
543
|
+
conflated `JValue::Null` with `JValue::Undefined`, so `arr[p > 1]` over
|
|
544
|
+
`[{"p": 1}, {"p": null}]` silently returned undefined where jsonata-js raises `T2010`.
|
|
545
|
+
Rewritten to the same rule -- only numbers, strings and undefined are comparable; an
|
|
546
|
+
undefined operand yields undefined; a type mismatch is `T2009`.
|
|
547
|
+
- `$sort` comparators of the form `function($l, $r) { $l.f > $r.f }` no longer sort inputs
|
|
548
|
+
that jsonata-js rejects. The specialized Schwartzian-transform fast path collapsed every
|
|
549
|
+
non-numeric, non-string key into "missing" and treated mixed types as "keep original
|
|
550
|
+
order"; it now declines those inputs so the general comparator raises `T2010`/`T2009`.
|
|
551
|
+
Absent keys are still undefined and still sort last on the fast path.
|
|
552
|
+
([#102](https://github.com/txjmb/jsonata-core/issues/102), cluster A)
|
|
553
|
+
- `$map` and `$filter` now return sequences rather than arrays: a single result unwraps to
|
|
554
|
+
that result and an empty result is undefined, so `$map(arr, function($v){$v.p})` over
|
|
555
|
+
`[{"p": "free"}]` is `"free"` rather than `["free"]`, and over `[]` is undefined rather
|
|
556
|
+
than `[]`. `$map` also accepts a non-array argument as the singleton sequence containing
|
|
557
|
+
it, which `$filter` already did.
|
|
558
|
+
- Field access on a lambda parameter (`$v.p`, `$l.rating`) now yields undefined for a missing
|
|
559
|
+
field instead of null. The `$var.field` fast path used by sort and higher-order-function
|
|
560
|
+
bodies predates the null/undefined split, which is why `$map` produced `[1, null]` where
|
|
561
|
+
jsonata-js drops the undefined, and why `$filter` raised `T2010` comparing what was really
|
|
562
|
+
a missing field. ([#102](https://github.com/txjmb/jsonata-core/issues/102), cluster B)
|
|
563
|
+
- `!=` against an undefined operand is now `false`, matching `=`. jsonata-js returns false
|
|
564
|
+
for both when either side is undefined -- `!=` is not the negation of `=` there -- so
|
|
565
|
+
`arr[p != null]` no longer keeps elements whose `p` is missing.
|
|
566
|
+
- Object construction as a path step now follows sequence semantics: `arr.{"k": p}` over a
|
|
567
|
+
single element is the object rather than a one-element array, and over a non-array value it
|
|
568
|
+
builds from that value instead of from the root document (previously `{}`).
|
|
569
|
+
([#102](https://github.com/txjmb/jsonata-core/issues/102), cluster C)
|
|
570
|
+
- `&` now stringifies an explicit `null` as `"null"`, matching `$string(null)`, and treats
|
|
571
|
+
only an *undefined* operand as the empty string. `null & "x"` was `"x"` and is now
|
|
572
|
+
`"nullx"`; `missing.x & "x"` is still `"x"`.
|
|
573
|
+
- `in` is membership again, not array filtering. An array on the left made `evaluate_binary_op`
|
|
574
|
+
treat the expression as `array[predicate]`, so `arr in 1` evaluated as `arr[1]` and returned
|
|
575
|
+
an element. It now follows jsonata-js: an undefined operand on either side gives `false`, a
|
|
576
|
+
non-array right side is wrapped, and membership is decided with `===` -- primitives by value,
|
|
577
|
+
composites by identity. `obj in [obj]` is true, `obj in [{"k": 1}]` is false, and an object
|
|
578
|
+
on the right is no longer treated as key-containment (`"k" in obj` is `false`).
|
|
579
|
+
- Division and modulo by zero no longer raise. jsonata-js checks operands, never results:
|
|
580
|
+
`1/0` is `Infinity` and `0/0` is `NaN`, and the `D1001` appears when such a value is used as
|
|
581
|
+
an operand (`1/(10e300 * 10e100)`) or serialised inside a composite
|
|
582
|
+
(`$string({"inf": 1/0})`). The multiply overflow check moved from the result to the operands
|
|
583
|
+
to match. JSON cannot spell Infinity, so the JSON-returning APIs give `null` for it, exactly
|
|
584
|
+
as JavaScript's `JSON.stringify` does.
|
|
585
|
+
- Unary negation of an explicit `null` now raises `D1002` instead of returning null; only
|
|
586
|
+
*undefined* propagates.
|
|
587
|
+
|
|
588
|
+
### Security
|
|
589
|
+
|
|
590
|
+
## [2.2.5] - 2026-07-14
|
|
591
|
+
|
|
592
|
+
### Added
|
|
593
|
+
- C API (`capi` cargo feature): use the engine from C, C++, or any language with C interop.
|
|
594
|
+
Eight functions, JSON text in/out (`jsonata_compile`, `jsonata_evaluate`, `jsonata_bind_var`,
|
|
595
|
+
`jsonata_free_expr`, `jsonata_free_string`, `jsonata_last_error_message`,
|
|
596
|
+
`jsonata_last_error_code`, `jsonata_version`), thread-local error slot, and engine panics
|
|
597
|
+
caught at the boundary instead of aborting the host process. Ships with a hand-written
|
|
598
|
+
header (`bindings/c/jsonata.h`), build/link/CMake documentation (`bindings/c/README.md`),
|
|
599
|
+
and a CI-gated smoke test compiled as both C and C++. Build with
|
|
600
|
+
`cargo build --release --features capi`.
|
|
601
|
+
|
|
602
|
+
### Fixed
|
|
603
|
+
- Performance regression in v2.2.4 on small/fast expressions (issue #74): the
|
|
604
|
+
`JSONATAPY_FORCE_TREE_WALKER` test toggle read the environment variable on every
|
|
605
|
+
evaluation (both `evaluate()` and `evaluate_json()`), costing ~100-200ns per call —
|
|
606
|
+
10-30% of a sub-microsecond expression. The toggle is now a process-wide atomic seeded
|
|
607
|
+
from the environment once at import (whole-process forcing works unchanged) and flippable
|
|
608
|
+
via a private test hook. Small-expression benchmarks recover 5-16%; the remaining few
|
|
609
|
+
percent vs v2.2.3 on tiny payloads is the documented cost of lazy conversion (which makes
|
|
610
|
+
realistic workloads up to 48% faster, see 2.2.4 notes).
|
|
611
|
+
|
|
612
|
+
### Changed
|
|
613
|
+
- Benchmark tooling: PR benchmark comments and release regression issues now state their
|
|
614
|
+
comparison baseline explicitly (which commit/release, recorded when, on which runner),
|
|
615
|
+
and the vs-jsonata-js comparison is labeled as such.
|
|
616
|
+
|
|
617
|
+
## [2.2.4] - 2026-07-13
|
|
618
|
+
|
|
619
|
+
### Added
|
|
620
|
+
- `JSONATAPY_FORCE_TREE_WALKER=1` environment variable (testing/debugging): forces every
|
|
621
|
+
evaluation through the tree-walking evaluator, bypassing the default lazy/VM-preferred path.
|
|
622
|
+
|
|
623
|
+
### Changed
|
|
624
|
+
- `evaluate(dict)` now converts Python data lazily by default. Previously every call eagerly
|
|
625
|
+
converted the entire input `dict` (and nested structures) to the internal value tree before
|
|
626
|
+
evaluation began; now only the fields an expression actually touches are converted, and
|
|
627
|
+
untouched input subtrees pass straight through to the output unchanged. Measured on the dev
|
|
628
|
+
machine (min-of-5, vs. the prior eager `evaluate(dict)`):
|
|
629
|
+
- `products.price` (100 objects): 34.9µs → 11.3µs (3.1x)
|
|
630
|
+
- Filter by category (100 products, 9 fields each): 129.3µs → 23.4µs (5.5x)
|
|
631
|
+
- `$sum(products[inStock].price)`: 116.4µs → 17.1µs (6.8x)
|
|
632
|
+
- Complex dense transformation: 160.9µs → 79µs (2.0x)
|
|
633
|
+
|
|
634
|
+
Every measured row now beats jsonata-js on identical data and machine (14.6/93.7/67.6/415.1µs
|
|
635
|
+
respectively). See `benchmarks/python/lazy_check.py` for the reproducible gate.
|
|
636
|
+
- **Behavior change:** results containing an unmodified input subtree now reference the
|
|
637
|
+
caller's *original* Python `dict`/`list` objects (result aliasing, matching jsonata-js),
|
|
638
|
+
rather than a fresh copy. Mutating such a result mutates the corresponding input — copy
|
|
639
|
+
explicitly (e.g. `copy.deepcopy`) first if you plan to mutate. Passed-through values also keep
|
|
640
|
+
their exact Python type (an `int` field the expression never reads stays an `int`); fields the
|
|
641
|
+
expression does touch still round-trip through the engine's number representation (whole
|
|
642
|
+
values come back as Python `int`), as before.
|
|
643
|
+
- **Behavior change:** an unconvertible input value (e.g. a `set`) now raises `TypeError` only
|
|
644
|
+
when the expression actually touches it, instead of eagerly for the whole input at the start
|
|
645
|
+
of `evaluate()`. Any Python exception raised while lazily reading a field (e.g. `OverflowError`
|
|
646
|
+
on an integer too large to represent) is likewise normalized to `TypeError` at this boundary,
|
|
647
|
+
rather than propagating as its original exception type.
|
|
648
|
+
|
|
649
|
+
### Deprecated
|
|
650
|
+
|
|
651
|
+
### Removed
|
|
652
|
+
|
|
653
|
+
### Fixed
|
|
654
|
+
|
|
655
|
+
### Security
|
|
656
|
+
|
|
657
|
+
## [2.2.3] - 2026-07-12
|
|
658
|
+
|
|
659
|
+
### Added
|
|
660
|
+
- `evaluate_json_or_none()`'s `json_str` parameter now accepts `None` (in addition to a JSON
|
|
661
|
+
string), binding the top-level context (`$`) to a true JSONata `Undefined` rather than an
|
|
662
|
+
explicit `null`. (#68)
|
|
663
|
+
- `docs/cli.md`: a full command-line reference for both `jsonata` (Rust) and `jsonatapy`
|
|
664
|
+
(Python), covering flags, input resolution, output/exit-code semantics, and the MCP
|
|
665
|
+
subcommand — previously undocumented outside internal spec/plan files. (#68)
|
|
666
|
+
|
|
667
|
+
### Changed
|
|
668
|
+
- Release CLI binary archives are now named `jsonata-v<version>-<target>.tar.gz`/`.zip`
|
|
669
|
+
(previously `jsonata-<target>.tar.gz`/`.zip` with no version embedded).
|
|
670
|
+
|
|
671
|
+
### Deprecated
|
|
672
|
+
|
|
673
|
+
### Removed
|
|
674
|
+
|
|
675
|
+
### Fixed
|
|
676
|
+
- The Python CLI's `-n`/`--null-input` now binds `$` to a true `Undefined`, matching the Rust
|
|
677
|
+
CLI exactly (`jsonatapy -n '$'` now prints nothing, as `jsonata -n '$'` already did). It
|
|
678
|
+
previously passed an explicit JSON `null` context instead, observable only for expressions
|
|
679
|
+
referencing `$` directly. (#68)
|
|
680
|
+
- Release workflow: `publish-pypi` and `publish-crates` now require `build-cli-binaries` to
|
|
681
|
+
succeed first. Previously the two registry publishes were independent of the CLI binary
|
|
682
|
+
build, so a CLI build failure (untested in a real release prior to this) would have shipped
|
|
683
|
+
a version to PyPI/crates.io with no CLI binaries attached and no way to reuse that version
|
|
684
|
+
number.
|
|
685
|
+
|
|
686
|
+
### Security
|
|
687
|
+
|
|
688
|
+
## [2.2.2] - 2026-07-09
|
|
689
|
+
|
|
690
|
+
### Added
|
|
691
|
+
- Documented `JsonataData`, `evaluate_with_data`, and `evaluate_data_to_json` in `docs/api.md`
|
|
692
|
+
(previously absent from the API reference entirely, despite being the "3-15x faster"
|
|
693
|
+
pre-converted-data path highlighted in the README's Performance section). Added a cross-link
|
|
694
|
+
from `docs/rust-crate.md` to the full auto-generated API reference on docs.rs. (#65)
|
|
695
|
+
|
|
696
|
+
### Changed
|
|
697
|
+
- Updated Rust dependencies to the latest versions compatible with existing (unpinned) semver
|
|
698
|
+
requirements: `simd-json` 0.17.0 → 0.17.2 plus 6 transitive patch bumps. No security advisories
|
|
699
|
+
found before or after (`cargo deny check`). (#65)
|
|
700
|
+
|
|
701
|
+
### Deprecated
|
|
702
|
+
|
|
703
|
+
### Removed
|
|
704
|
+
|
|
705
|
+
### Fixed
|
|
706
|
+
- SIMD-accelerated JSON parsing (`simd-json` feature, on by default) was consistently *slower*
|
|
707
|
+
than the plain `serde_json` fallback for most payload sizes (up to 29% slower at 180KB), the
|
|
708
|
+
opposite of its intent — caused by allocating fresh internal scratch buffers on every single
|
|
709
|
+
parse call. Fixed by reusing a thread-local scratch buffer across calls; SIMD parsing now beats
|
|
710
|
+
`serde_json` consistently (up to +22% faster) instead of losing at 3 of 4 tested sizes.
|
|
711
|
+
Also corrected the README's "(optional feature)" wording for SIMD, which implied opt-in when
|
|
712
|
+
it's actually enabled by default, including in published wheels. (#65)
|
|
713
|
+
|
|
714
|
+
### Security
|
|
715
|
+
|
|
716
|
+
## [2.2.1] - 2026-07-08
|
|
717
|
+
|
|
718
|
+
> Same code as [2.1.7](#217---2026-07-08) below, renumbered. This project's release versions
|
|
719
|
+
> track the jsonata-js major/minor version they target (patch numbers are independent — see
|
|
720
|
+
> README). `2.1.7` incorrectly continued the old `2.1.x` patch series even though this release's
|
|
721
|
+
> guardrails feature and signature-engine fixes target jsonata-js `2.2.0`/`2.2.1`; `2.1.7` is
|
|
722
|
+
> superseded immediately by this release and should not be used.
|
|
723
|
+
|
|
724
|
+
### Added
|
|
725
|
+
- Guardrails: `timeout` (ms, error code `D1012`), `max_stack_depth` (error code `D1011`), and
|
|
726
|
+
`max_sequence_length` (error code `D2015`) keyword arguments on `compile()` and every
|
|
727
|
+
`evaluate*()` call, enforced consistently across all three execution engines (tree-walker,
|
|
728
|
+
compiled-expression fast path, bytecode VM). All default to `None` (unlimited) — no behavior
|
|
729
|
+
change unless configured. See [Guardrails](docs/api.md#guardrails). (jsonata-js 2.2.1 Phase 2, #56)
|
|
730
|
+
- Documented the guardrails API in `docs/api.md`, `docs/usage.md`, and `docs/error-handling.md`
|
|
731
|
+
(previously shipped with accurate Python docstrings but no user-facing docs), and corrected
|
|
732
|
+
`docs/migration-from-js.md`'s stale claim that Python had no built-in timeout support.
|
|
733
|
+
|
|
734
|
+
### Fixed
|
|
735
|
+
- A deeply-nested expression (arithmetic chains, parenthesized/grouped expressions) no longer
|
|
736
|
+
crashes the whole process (previously a native stack overflow) — now raises a graceful `U1002`
|
|
737
|
+
error instead, via a depth guard in the parser and a second, defense-in-depth guard in the
|
|
738
|
+
post-parse AST pass.
|
|
739
|
+
- `Instr::MakeArray`/`MakeObject`/`BlockEnd`'s bytecode operands (and `CallBuiltin`'s argument
|
|
740
|
+
count, and internal constant-pool bookkeeping) no longer silently produce wrong, truncated
|
|
741
|
+
results for oversized literals/blocks/calls (e.g. array literals with more than 65,535
|
|
742
|
+
elements) — such cases now fall back to the always-correct tree-walker instead.
|
|
743
|
+
- `ast_transform.rs`'s depth-guard error messages no longer imply `%`/`@`/`#` ancestor-operator
|
|
744
|
+
usage (e.g. "...while resolving ancestor/path metadata") when the guard fires for any
|
|
745
|
+
sufficiently-nested expression, including plain arithmetic.
|
|
746
|
+
- Release workflow now fails loudly on a fresh dispatch when the target version tag already
|
|
747
|
+
exists at a different commit, instead of silently reusing the wrong commit (#53).
|
|
748
|
+
|
|
749
|
+
## [2.1.7] - 2026-07-08
|
|
750
|
+
|
|
751
|
+
**Superseded by [2.2.1](#221---2026-07-08) above, published the same day.** This version was
|
|
752
|
+
numbered following a simple patch-increment from `2.1.6` rather than this project's actual
|
|
753
|
+
versioning policy (track jsonata-js's major/minor). It is fully functional and was not yanked —
|
|
754
|
+
package registries don't allow deleting a published version — but `2.2.1` is the version that
|
|
755
|
+
should be used going forward.
|
|
756
|
+
|
|
757
|
+
### Added
|
|
758
|
+
- Guardrails: `timeout` (ms, error code `D1012`), `max_stack_depth` (error code `D1011`), and
|
|
759
|
+
`max_sequence_length` (error code `D2015`) keyword arguments on `compile()` and every
|
|
760
|
+
`evaluate*()` call, enforced consistently across all three execution engines (tree-walker,
|
|
761
|
+
compiled-expression fast path, bytecode VM). All default to `None` (unlimited) — no behavior
|
|
762
|
+
change unless configured. See [Guardrails](docs/api.md#guardrails). (jsonata-js 2.2.1 Phase 2, #56)
|
|
763
|
+
- Documented the guardrails API in `docs/api.md`, `docs/usage.md`, and `docs/error-handling.md`
|
|
764
|
+
(previously shipped with accurate Python docstrings but no user-facing docs), and corrected
|
|
765
|
+
`docs/migration-from-js.md`'s stale claim that Python had no built-in timeout support.
|
|
766
|
+
|
|
767
|
+
### Fixed
|
|
768
|
+
- A deeply-nested expression (arithmetic chains, parenthesized/grouped expressions) no longer
|
|
769
|
+
crashes the whole process (previously a native stack overflow) — now raises a graceful `U1002`
|
|
770
|
+
error instead, via a depth guard in the parser and a second, defense-in-depth guard in the
|
|
771
|
+
post-parse AST pass.
|
|
772
|
+
- `Instr::MakeArray`/`MakeObject`/`BlockEnd`'s bytecode operands (and `CallBuiltin`'s argument
|
|
773
|
+
count, and internal constant-pool bookkeeping) no longer silently produce wrong, truncated
|
|
774
|
+
results for oversized literals/blocks/calls (e.g. array literals with more than 65,535
|
|
775
|
+
elements) — such cases now fall back to the always-correct tree-walker instead.
|
|
776
|
+
- `ast_transform.rs`'s depth-guard error messages no longer imply `%`/`@`/`#` ancestor-operator
|
|
777
|
+
usage (e.g. "...while resolving ancestor/path metadata") when the guard fires for any
|
|
778
|
+
sufficiently-nested expression, including plain arithmetic.
|
|
779
|
+
- Release workflow now fails loudly on a fresh dispatch when the target version tag already
|
|
780
|
+
exists at a different commit, instead of silently reusing the wrong commit (#53).
|
|
781
|
+
|
|
782
|
+
## [2.1.6] - 2026-07-07
|
|
783
|
+
|
|
784
|
+
### Added
|
|
785
|
+
- `%` (parent-reference) and `@`/`#` (focus/index binding) operators.
|
|
786
|
+
- jsonata-js 2.2.1 Phase 1: signature engine rewrite adding `+`/`-` arity support (#36).
|
|
787
|
+
- Versioned documentation via `mike`, fixing a gh-pages deploy race (#39).
|
|
788
|
+
|
|
789
|
+
### Fixed
|
|
790
|
+
- Reference test-suite coverage gaps: loader fix, datetime picture-strings,
|
|
791
|
+
`formatInteger`/`parseInteger` (Phases 0-2), and array-constructor/`distinct` stragglers
|
|
792
|
+
(Phase 5, #44).
|
|
793
|
+
- `release.yml` never actually built macOS wheels, due to a broken `actions/setup-python`
|
|
794
|
+
invocation (#41).
|
|
795
|
+
- Several benchmark accuracy/fairness corrections (await `jsonata-js` calls properly, substantiate
|
|
796
|
+
pre-converted-data speedup claims, use clean CI-sourced numbers, use jsonata-python's `Context`
|
|
797
|
+
for fair repeated-eval timing) (#45, #46, #49, #50).
|
|
798
|
+
|
|
799
|
+
### Changed
|
|
800
|
+
- macOS temporarily dropped from the main release matrix pending a self-hosted runner fix (#45,
|
|
801
|
+
#47) — later restored via a self-hosted Mac Mini runner.
|
|
802
|
+
|
|
803
|
+
## [2.1.5] - 2026-07-04
|
|
804
|
+
|
|
805
|
+
### Fixed
|
|
806
|
+
- Native stack overflow on deep recursion — replaced with a graceful, coded error (fixes #34).
|
|
807
|
+
- Lambda IDs are now generated from a monotonic counter instead of an AST pointer address, fixing
|
|
808
|
+
a ~0.5%-frequency wrong-closure bug from id aliasing across recursive/repeated evaluation
|
|
809
|
+
(fixes #35).
|
|
810
|
+
- Tree-walker missing-path/field access now correctly produces `Undefined` instead of `Null` in
|
|
811
|
+
the ~20 sites that predated the `Null`/`Undefined` distinction (fixes #32).
|
|
812
|
+
- CI repairs following the default-branch rename to `main`; patched `RUSTSEC-2026-0097`.
|
|
813
|
+
|
|
814
|
+
### Changed
|
|
815
|
+
- Bumped `pyo3` to 0.29 and `rand` to 0.10.
|
|
816
|
+
|
|
817
|
+
## [2.1.4] - 2026-03-22
|
|
818
|
+
|
|
819
|
+
### Added
|
|
820
|
+
- `cargo publish` step in the release workflow.
|
|
821
|
+
|
|
822
|
+
### Fixed
|
|
823
|
+
- Release workflow is now idempotent for version-bump and tag-creation steps.
|
|
824
|
+
- Corrected an incorrect expected value in the `substring` test suite.
|
|
825
|
+
|
|
826
|
+
## [2.1.3] - 2026-03-22
|
|
827
|
+
|
|
828
|
+
> Versions 2.1.1 and 2.1.2 were bumped internally but never published as standalone tagged
|
|
829
|
+
> releases — their changes are folded into this entry, the next version actually released.
|
|
830
|
+
|
|
831
|
+
### Added
|
|
832
|
+
- Bytecode VM (`compiler.rs` + `vm.rs`, "Phase 4") restored and wired into the Python execution
|
|
833
|
+
path, with Criterion benchmarks comparing it against the tree-walker.
|
|
834
|
+
- `pyo3` made an optional dependency; Rust crate renamed to `jsonata-core` and published to
|
|
835
|
+
crates.io independently of the `jsonatapy` PyPI package.
|
|
836
|
+
|
|
837
|
+
### Fixed
|
|
838
|
+
- PyO3 0.28 compatibility (`PyObject` → `Py<PyAny>`).
|
|
839
|
+
- Clippy deprecations and `cargo-deny` license-check failures.
|
|
840
|
+
- Upgraded Pillow (dev/docs dependency) to 12.1.1 for a CVE fix.
|
|
841
|
+
|
|
842
|
+
### Changed
|
|
843
|
+
- Multiple benchmark documentation and accuracy corrections.
|
|
844
|
+
|
|
845
|
+
## [2.1.0] - 2026-02-08
|
|
846
|
+
|
|
847
|
+
### Added
|
|
848
|
+
- Initial public release: Rust-based JSONata implementation targeting jsonata-js v2.1.0 semantics.
|
|
849
|
+
- Full jsonata-js v2.1.0 reference test-suite compatibility.
|
|
850
|
+
- Python bindings (PyO3), published as `jsonatapy` on PyPI.
|
|
851
|
+
- Rust core, published as `jsonata-core` on crates.io.
|
|
852
|
+
|
|
853
|
+
---
|
|
854
|
+
|
|
855
|
+
## Reference Implementation Tracking
|
|
856
|
+
|
|
857
|
+
This project tracks the [jsonata-js](https://github.com/jsonata-js/jsonata) reference implementation.
|
|
858
|
+
|
|
859
|
+
**Current status:** Full test-suite compatibility with jsonata-js v2.1.0 (+3 commits). jsonata-js
|
|
860
|
+
v2.2.1 compatibility work is in progress — Phase 1 (signature engine, `+`/`-` arity support) and
|
|
861
|
+
Phase 2 (resource guardrails) are done; see
|
|
862
|
+
`docs/superpowers/specs/2026-07-04-jsonata-2.2.1-design.md` for the authoritative, up-to-date
|
|
863
|
+
status of this effort.
|
|
864
|
+
|
|
865
|
+
### Version History
|
|
866
|
+
- Target tracking v2.1.0 - Project initialization (2025-01-17)
|