You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
BREAKING (C ABI — in-tree consumers only): ABI v2.bindings/c
replaces the v1 contract wholesale: (pointer, length) UTF-8 inputs
(no NUL terminators anywhere), status-code returns with an optional datalogic_error ** out-param (the thread-local last-error block is
deleted, and with it Go's per-call LockOSThread), borrowed
session results, owned datalogic_buf one-shot results, and a
custom-operator callback protocol with no cross-boundary allocator
handoff. Wrappers assert datalogic_abi_version() == 2 at load. The
ABI was never published as a standalone artifact; all four in-tree
consumers (Go, JVM, .NET, PHP) migrated in lockstep with their public
APIs unchanged. Migration table in MIGRATION.md.
BREAKING (JVM environment): FFM replaces JNA; JDK 22+ required.
The Java binding now reaches the C ABI through java.lang.foreign
(add --enable-native-access=ALL-UNNAMED on JDK 24+); the JNA
dependency is deleted and with it the microseconds of reflective
dispatch per call. The Java-visible API is unchanged, and the JAR's
native-resource layout is identical. Also fixes by construction the
latent non-ASCII corruption: JNA marshalled argument strings with
the JVM default charset (results were already forced UTF-8).
BREAKING (WASM): errors are real Error objects. Through 5.0.0 every @goplasmatic/datalogic-wasm
API rejected with a plain JSON string, so e instanceof Error was false. APIs now throw a proper Error whose name is the error
kind (for example "ParseError"), with the structured fields
(type, operator, node_ids, variant extras) attached as own
properties and the old JSON string preserved verbatim on e.detailJson. Migration snippets in bindings/wasm/README.md.
Performance
C-family bindings: session results serialize into a reusable
session-owned buffer and cross the boundary as borrowed bytes — the
per-result malloc and the datalogic_string_free crossing are gone;
session-less one-shots run over a pooled thread-local arena, so naive
callers get session-grade allocation behaviour.
Node: the object-typed entry points (Rule.evaluate, Engine.eval)
now route JSON-string data straight into the arena parser instead of
building an intermediate serde_json::Value tree (mirroring what evaluateStr and the Session methods already did).
Python: wheels now build with fat LTO + a single codegen unit — the
binding's standalone workspace previously shipped with no release
profile at all, losing cross-crate inlining into the core.
Python: dict inputs and results convert via a direct walk between
Python objects and arena values instead of the pythonize double tree
(pythonize retained only as the exotic-shape fallback), with the
pre-change semantics pinned by a 549-case equivalence corpus:
2.5-3.4x faster at every payload size, and the 8 KB dict path drops
from ~82 µs to ~24 µs — now ~3x faster than a json.dumps / json.loads round-trip. (The same direct-converter approach was
built, measured, and deliberately reverted for Node: 23-31% faster
than its serde bridge but still structurally slower than V8's JSON.stringify + one string crossing; the string path remains
Node's fast lane, and the equivalence test stays in-tree as the gate
for future attempts.)
Wide-object key lookup uses an optimistic ordered probe.
Identically-shaped ISO datetime strings compare on a byte-compare
fast path.
try/catch propagates thrown values through a borrowed channel and
interns the NaN payload.
Composite literals are pre-converted at compile time via
self-referential cells.
Strings render directly into the evaluation arena (no intermediate
heap String); contiguous slice (step == 1) is zero-copy; hot
numeric/flagd paths and cold error/output paths drop throwaway
allocations; constant-fold passes early-bail before cloning arg trees.
Fixed
Docs honesty: the Python README's dict-conversion claim is qualified
by payload size (the pythonize path wins below roughly 1 KB and
reverses above), and the WASM README no longer claims zero-copy
strings across the JS↔WASM boundary (both directions copy; cost
scales with payload size).
WASM binding: the now operator trapped with "time not implemented on
this platform" (and leaked that call's arena) in every JS host, because
the v5 rewrite dropped the v4 wasm opt-in for chrono/wasmbind. The
binding now enables the new wasm-clock feature, and a wasm-bindgen regression test covers the operator.
Benchmark harness: datalogic-bench was missing the flagd feature,
so flagd/*.json suites reported ERR for the native engine column
while the WASM column ran them.
Four conformance suites that existed on disk but were missing from tests/suites/index.json now run in the conformance runner; type.json array cases corrected.
Benchmark harness suites_root repointed at crates/datalogic-rs
after the repository reorganization.
Python binding: pyo3 / pythonize bumped to 0.29 (security advisories).
Added
ParsedData (core) — self-contained parse-once data handle,
accepted by every arena-lifetime evaluation entry point at zero
per-call conversion cost. Parsing dominates the string contract
(70-90% of a parse-eval-serialize round trip), and this factors it
out. Engine::truthy exposes the engine's configured truthiness
coercion for binding use.
Data handles, typed results, and batch evaluation across the
C-family bindings (C, Go, JVM, .NET, PHP): parse a payload once
(datalogic_data_parse / DataHandle) and evaluate many rules
against it; typed scalar evaluations (bool / i64 / f64 /
truthiness); and one-crossing batch shapes — one rule × N payloads
(evaluate_batch) and N rules × one payload (evaluate_many, the
rule-set/feature-flag shape) — with per-item error reporting that
never fails the whole call.
PHP FFI preload support — an FFI::load-compatible header and preload.php for opcache.preload + ffi.enable=preload
deployments; FFI::cdef remains the zero-config fallback.
In-tree boundary benchmark harness
(tools/benchmark/boundary/) — one runner per runtime (Rust core, C,
Go, JVM, .NET, PHP, Python, Node, WASM) reproducing the
BINDINGS-OVERHEAD methodology with byte-stable checked-in workloads,
a driver script, and a table renderer, so the per-binding overhead
numbers are reproducible with one command instead of living outside
the repo.
ABI v2 mirrors for Node, Python, and WASM — the direct-core
bindings gain the same tiers natively: DataHandle parse-once
handles, typed session evaluations (evaluateBool / number / truthy;
Python adds evaluate_int), and Promise.allSettled-shaped evaluateBatch / evaluateMany with per-item errors that never fail
the call. The WASM handle keeps the payload resident in linear
memory, so the per-call JS↔WASM copy + parse disappears (the 8 KB
session path drops ~7.7x).
Node async tier — Rule.evaluateStrAsync(dataJson) evaluates on
the libuv thread pool and returns a Promise<string>; rejections
carry the same structured fields as synchronous throws. Not faster
per call — it exists for event-loop hygiene on large payloads.
WASM speed-profile opt-in — WASM_PROFILE=speed ./build.sh
builds opt-level = 3 + wasm-opt -O3: measured 1.13-1.85x faster
across tiers at +8.1% raw size and 1.4% smaller gzipped. The
published default stays the size-optimized build.
wasm-clock feature — opt-in JS-host clock for the now operator
on wasm32-unknown-unknown (forwards to chrono/wasmbind; successor
to the v4 wasm feature). Off by default so non-JS wasm runtimes
(wasmtime, wazero, Chicory) keep loading the module — the constraint
from #47 —
with a CI guard asserting the default wasm32 dependency graph stays
free of wasm-bindgen/js-sys.
Logic::is_constant — reports whether compilation constant-folded
the entire rule to a literal. Complements Logic::is_static
(is_static asks whether a rule could be evaluated without a data
context; is_constant reports whether the compiler actually did
reduce it — folding can fail, e.g. {"/": [1, 0]} stays an operator
node so the error surfaces at evaluation time). The benchmark harness
uses it to time folded and non-folded rules separately.
EvaluationConfig::from_json_str (requires serde_json) — build a
configuration from a JSON object: an optional "preset" key
("default" / "safe_arithmetic" / "strict") plus per-field
overrides. This is the wire format the language bindings use to pass
engine configuration across FFI boundaries through one shared parser.
Unknown keys and enum strings are rejected loudly.
cargo-fuzz target over eval_str.
flagd fractional testbed scenarios (flagd v3.1.0–v3.5.0) ported into
the conformance suites.
Runnable examples/ for every language binding (C, Node, WASM,
Python, Go, JVM, .NET, PHP): the same three programs — getting-started, compile-once-evaluate-many, custom-operator — with the same rule
and data in each language, executed in CI so they cannot rot.
scripts/conformance-count.sh — generates the canonical
"N suites / M cases" statistic quoted in READMEs and release notes.
Signal Board redesign of the React visual debugger
(@goplasmatic/datalogic-ui): nodes are typed and coloured by the return-type signal they produce (boolean, number, string, array, and
so on) rather than by operator category, exposed through overridable --sig-* CSS tokens scoped to .logic-editor. Boolean operators render
as SVG logic-gate silhouettes (AND / OR / NOT), edges carry an explicit
left-to-right flow direction, and the depth axis packs by real node
widths so wide nodes no longer overlap. The React component's public
props are unchanged.
Removed
NumericCoercionConfig::undefined_to_zero and its with_undefined_to_zero setter. The flag was documented as reserved
and was never read: JSONLogic does not distinguish a missing key from
an explicit null, and a missing var already coerces to 0 under the
default null_to_zero = true. Removing an inert public field is
technically breaking for code that merely named it; delete the field
access or setter call — nothing changes behaviourally.