Skip to content

v5.0.1

Choose a tag to compare

@github-actions github-actions released this 15 Jul 03:04
· 303 commits to main since this release

Changed

  • 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.
  • Release platform matrix evened out: Intel-mac Python wheels and Node
    prebuilds, aarch64-musl Node prebuilds.
  • 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.