datalogic-rs 5.1.0

High-performance JSONLogic (json-logic) rules engine and sandboxed expression evaluator in Rust — one core, official bindings for Node.js, WASM, Python, Go, Java, .NET, and PHP. Compile once, evaluate in nanoseconds.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
//! Compiled node tree shared between the compile pipeline and the dispatch
//! engine. Submodules are structured by concern:
//!
//! - [`compile_ctx`] — `NodeId` encoding, the synthetic-id sentinel,
//!   and the per-compile id counter.
//! - [`payload`] — boxed payload structs and small helper enums
//!   referenced by `CompiledNode` variants.
//! - [`populate`] — post-compile pass that caches per-operator analysis
//!   results onto every `BuiltinOperator` node and pre-builds composite
//!   literals onto every `Value` node.
//! - [`prelit`] — `PreLit`, the pre-built literal view returned by the
//!   dispatch literal fast path without per-call arena work.
//! - [`logic`] — `Logic`, the public compiled-rule snapshot, and the
//!   static-evaluation predicates the optimizer consults.
//!
//! Re-exports below preserve the pre-split `crate::node::*` import paths so
//! callers elsewhere in the crate are unaffected by the file split.

mod compile_ctx;
mod logic;
mod payload;
mod populate;
mod prelit;

pub use logic::Logic;

pub(crate) use compile_ctx::{CompileCtx, NodeId, SYNTHETIC_ID};
pub(crate) use logic::node_is_static;
#[cfg(feature = "ext-control")]
pub(crate) use payload::CompiledExistsData;
#[cfg(feature = "error-handling")]
pub(crate) use payload::CompiledThrowData;
#[cfg(feature = "templating")]
pub(crate) use payload::StructuredObjectData;
pub(crate) use payload::{
    CompiledMissingArg, CompiledMissingData, CompiledMissingMin, CompiledMissingPaths,
    CompiledMissingSomeData, CseData, CustomOperatorData, MetadataHint, PathSegment, ReduceHint,
};
pub(crate) use populate::populate_lits;
pub(crate) use prelit::PreLit;

use crate::opcode::OpCode;
use datavalue::OwnedDataValue;
use populate::precompute_lit;
use std::num::NonZeroU32;

/// A compiled node representing a single operation or value in the logic tree.
///
/// Nodes are created during the compilation phase and evaluated during execution.
/// Each node type is optimized for its specific purpose:
///
/// - **Value**: Static JSON values that don't require evaluation
/// - **Array**: Collections of nodes evaluated sequentially
/// - **BuiltinOperator**: Fast OpCode-based dispatch for built-in operators
/// - **CustomOperator**: User-defined operators with dynamic dispatch
/// - **StructuredObject**: Template objects for structure preservation
#[derive(Debug, Clone)]
pub(crate) enum CompiledNode {
    /// A static JSON value that requires no evaluation.
    ///
    /// Used for literals like numbers, strings, booleans, and null —
    /// and for composite literals (arrays/objects) folded at compile time.
    ///
    /// `lit` holds a pre-built [`PreLit`] view of the literal. The arena
    /// dispatch hot path returns its borrow directly, skipping the
    /// per-call conversion and `arena.alloc`. Trivial literals
    /// (Null/Bool/Number/empty composites) are populated at node
    /// construction by [`populate::precompute_lit`]; non-trivial ones
    /// (non-empty Strings/Arrays/Objects) by the [`populate_lits`] pass
    /// at `Logic::new`, so runtime `synthetic_value` wrappers never pay
    /// the prebuild cost. `None` only for those synthetic composite
    /// wrappers, which fall through to `literal_fallback` at dispatch
    /// time. Read-only after compile — safe to share across threads via
    /// `Arc<Logic>`.
    Value {
        id: NodeId,
        value: OwnedDataValue,
        lit: Option<PreLit>,
    },

    /// An array of compiled nodes.
    ///
    /// Each node is evaluated in sequence, and the results are collected into a JSON array.
    /// Uses `Box<[CompiledNode]>` for memory efficiency.
    Array {
        id: NodeId,
        nodes: Box<[CompiledNode]>,
    },

    /// A built-in operator optimized with OpCode dispatch.
    ///
    /// The OpCode enum enables direct dispatch without string lookups,
    /// significantly improving performance for the 50+ built-in operators.
    ///
    /// `predicate_hint` caches the result of [`crate::operators::array::FastPredicate::try_detect_owned`]
    /// so quantifier/filter operators don't repeat the structural pattern
    /// match on every iteration. Populated post-compile by
    /// [`populate_lits`]; `None` for nodes that aren't a fast-predicate
    /// shape, and re-derived after every clone (the populate pass overwrites
    /// the field).
    ///
    /// `iter_arg_kind` caches the
    /// [`crate::operators::array::IterArgKind::classify`] result for `args[0]`
    /// when this op iterates (filter/map/all/some/none/reduce/merge/min/max).
    /// `IterArgKind::General` for everything else — the dispatcher reads the
    /// kind and forwards it to `resolve_iter_input`, sidestepping the per-call
    /// pattern match on the iterator input's shape. Re-derived on every
    /// populate-arena-lits pass so clones stay correct.
    BuiltinOperator {
        id: NodeId,
        opcode: OpCode,
        args: Box<[CompiledNode]>,
        predicate_hint: Option<Box<crate::operators::array::FastPredicate>>,
        iter_arg_kind: crate::operators::array::IterArgKind,
    },

    /// A custom operator registered via `Engine::add_operator`.
    /// Boxed to reduce enum size (rare variant).
    CustomOperator(Box<CustomOperatorData>),

    /// A structured object template for templating mode.
    /// Boxed to reduce enum size (rare variant).
    #[cfg(feature = "templating")]
    StructuredObject(Box<StructuredObjectData>),

    /// A pre-compiled variable access (unified var/val).
    ///
    /// scope_level 0 = current context (var-style), N = go up N levels (val with [[N], ...]).
    /// Segments are pre-parsed at compile time to avoid runtime string splitting.
    Var {
        id: NodeId,
        scope_level: u32,
        segments: Box<[PathSegment]>,
        reduce_hint: ReduceHint,
        metadata_hint: MetadataHint,
        default_value: Option<Box<CompiledNode>>,
    },

    /// A pre-compiled exists check.
    /// Boxed to reduce enum size (rare variant).
    #[cfg(feature = "ext-control")]
    Exists(Box<CompiledExistsData>),

    /// A pre-compiled throw with a static error object.
    /// Boxed to reduce enum size (rare variant).
    #[cfg(feature = "error-handling")]
    Throw(Box<CompiledThrowData>),

    /// A pre-compiled `missing` operator with paths parsed into segments.
    Missing(Box<CompiledMissingData>),

    /// A pre-compiled `missing_some` operator with paths parsed into segments
    /// and (where literal) min-count resolved.
    MissingSome(Box<CompiledMissingSomeData>),

    /// Compile-time placeholder for an operator invoked with malformed
    /// args (e.g. `and` / `or` / `if` with a non-array argument). The
    /// dispatcher raises an `InvalidArguments` error on encounter — this
    /// variant exists so the diagnostic surfaces at runtime via the
    /// normal error breadcrumb path rather than at compile time. The
    /// `op_name` is captured from the source-text op so the runtime error
    /// names *which* op was misused (e.g. "Invalid arguments: if") even
    /// when the failure is nested inside an outer op.
    InvalidArgs { id: NodeId, op_name: &'static str },

    /// A common-subexpression memo wrapper produced by the compile-time
    /// CSE pass (`crate::compile::optimize::cse`). Transparent everywhere
    /// except the dispatch hub: `dispatch_cse` consults/fills the
    /// per-evaluation memo slot when `ctx.depth() == 0` and no tracer is
    /// attached; every other consumer (id, serialization, static
    /// classification, trace tree, path walk) delegates to `inner`, so a
    /// wrapped tree is observably identical to an unwrapped one.
    /// Boxed to keep the enum within the 48-byte layout budget. Declared
    /// last so every pre-existing variant keeps its discriminant value.
    Cse(Box<CseData>),
}

impl CompiledNode {
    /// Returns the unique id assigned to this node during compilation, as
    /// the public `u32` shape used by trace/error breadcrumbs (`0` for
    /// synthetic nodes, matching the previous sentinel).
    ///
    /// IDs are shared across tracing and error breadcrumbs — one source of
    /// truth per node. Synthetic nodes built outside the compile pipeline
    /// (test helpers, `eager_apply` value wrappers) carry [`SYNTHETIC_ID`]
    /// and round-trip to `0` here.
    #[inline]
    pub(crate) fn id(&self) -> u32 {
        self.node_id().map(NonZeroU32::get).unwrap_or(0)
    }

    /// Returns the structured node id (real `Some(NonZero)` vs. synthetic
    /// `None`). Internal callers prefer this over [`Self::id`] when they
    /// need to distinguish the two cases.
    #[inline]
    pub(crate) fn node_id(&self) -> NodeId {
        match self {
            CompiledNode::Value { id, .. } => *id,
            CompiledNode::Array { id, .. } => *id,
            CompiledNode::BuiltinOperator { id, .. } => *id,
            CompiledNode::CustomOperator(data) => data.id,
            // The memo wrapper is id-transparent: breadcrumbs and trace
            // steps attribute to the wrapped node, byte-identical to an
            // unwrapped tree.
            CompiledNode::Cse(data) => data.inner.node_id(),
            #[cfg(feature = "templating")]
            CompiledNode::StructuredObject(data) => data.id,
            CompiledNode::Var { id, .. } => *id,
            #[cfg(feature = "ext-control")]
            CompiledNode::Exists(data) => data.id,
            #[cfg(feature = "error-handling")]
            CompiledNode::Throw(data) => data.id,
            CompiledNode::Missing(data) => data.id,
            CompiledNode::MissingSome(data) => data.id,
            CompiledNode::InvalidArgs { id, .. } => *id,
        }
    }

    /// Invoke `f` on each AST child of `self`, in JSONLogic-positional
    /// order, paired with the child's positional index (matching
    /// [`crate::PathStep::arg_index`] semantics).
    ///
    /// Single source of truth for "what are this node's children" — the
    /// post-compile populate pass, the static-byte estimator, and the
    /// path resolver all defer to this rather than pattern-matching every
    /// variant themselves.
    pub(crate) fn visit_indexed_children<F: FnMut(u32, &CompiledNode)>(&self, f: &mut F) {
        match self {
            CompiledNode::Value { .. } => {}
            CompiledNode::Array { nodes, .. } => {
                for (i, n) in nodes.iter().enumerate() {
                    f(i as u32, n);
                }
            }
            CompiledNode::BuiltinOperator { args, .. } => {
                for (i, n) in args.iter().enumerate() {
                    f(i as u32, n);
                }
            }
            CompiledNode::CustomOperator(data) => {
                for (i, n) in data.args.iter().enumerate() {
                    f(i as u32, n);
                }
            }
            // The memo wrapper presents `inner` as its single child.
            // Consumers that need full transparency (the path walker)
            // delegate to `inner` explicitly before their generic body.
            CompiledNode::Cse(data) => f(0, &data.inner),
            #[cfg(feature = "templating")]
            CompiledNode::StructuredObject(data) => {
                for (i, (_, n)) in data.fields.iter().enumerate() {
                    f(i as u32, n);
                }
            }
            CompiledNode::Var { default_value, .. } => {
                if let Some(d) = default_value {
                    f(0, d);
                }
            }
            #[cfg(feature = "ext-control")]
            CompiledNode::Exists(_) => {}
            #[cfg(feature = "error-handling")]
            CompiledNode::Throw(_) => {}
            CompiledNode::Missing(data) => {
                for (i, arg) in data.args.iter().enumerate() {
                    if let CompiledMissingArg::Later(n) = arg {
                        f(i as u32, n);
                    }
                }
            }
            CompiledNode::MissingSome(data) => {
                if let CompiledMissingMin::Later(n) = &data.min_present {
                    f(0, n);
                }
                if let CompiledMissingPaths::Later(n) = &data.paths {
                    f(1, n);
                }
            }
            CompiledNode::InvalidArgs { .. } => {}
        }
    }

    /// Mutable mirror of [`Self::visit_indexed_children`]. Used by the post-compile
    /// populate pass to walk the tree once for both per-variant local work
    /// and recursion.
    pub(crate) fn visit_children_mut<F: FnMut(&mut CompiledNode)>(&mut self, f: &mut F) {
        match self {
            CompiledNode::Value { .. } => {}
            CompiledNode::Array { nodes, .. } => {
                for n in nodes.iter_mut() {
                    f(n);
                }
            }
            CompiledNode::BuiltinOperator { args, .. } => {
                for n in args.iter_mut() {
                    f(n);
                }
            }
            CompiledNode::CustomOperator(data) => {
                for n in data.args.iter_mut() {
                    f(n);
                }
            }
            CompiledNode::Cse(data) => f(&mut data.inner),
            #[cfg(feature = "templating")]
            CompiledNode::StructuredObject(data) => {
                for (_, n) in data.fields.iter_mut() {
                    f(n);
                }
            }
            CompiledNode::Var { default_value, .. } => {
                if let Some(d) = default_value {
                    f(d);
                }
            }
            #[cfg(feature = "ext-control")]
            CompiledNode::Exists(_) => {}
            #[cfg(feature = "error-handling")]
            CompiledNode::Throw(_) => {}
            CompiledNode::Missing(data) => {
                for arg in data.args.iter_mut() {
                    if let CompiledMissingArg::Later(n) = arg {
                        f(n);
                    }
                }
            }
            CompiledNode::MissingSome(data) => {
                if let CompiledMissingMin::Later(n) = &mut data.min_present {
                    f(n);
                }
                if let CompiledMissingPaths::Later(n) = &mut data.paths {
                    f(n);
                }
            }
            CompiledNode::InvalidArgs { .. } => {}
        }
    }

    /// Convenience constructor for a `Value` node with a [`SYNTHETIC_ID`].
    ///
    /// Used by operator fast paths that wrap runtime values back into
    /// `CompiledNode::Value` purely for dispatch. These wrappers are never
    /// observed by tracing or error reporting, so assigning a real id would
    /// be misleading.
    #[inline]
    pub(crate) fn synthetic_value(value: OwnedDataValue) -> Self {
        Self::value_with_id(SYNTHETIC_ID, value)
    }

    /// Construct a `CompiledNode::Value` with `id` and `value`, populating
    /// the precomputed `lit` for primitive literals so the arena
    /// dispatch hot path can borrow it without a per-call `arena.alloc`.
    /// Centralised here so every construction site stays in sync — adding
    /// a new precomputable variant only requires editing
    /// [`populate::precompute_lit`].
    #[inline]
    pub(crate) fn value_with_id(id: NodeId, value: OwnedDataValue) -> Self {
        let lit = precompute_lit(&value);
        CompiledNode::Value { id, value, lit }
    }

    /// [`Self::value_with_id`] plus an eager [`PreLit`] build for
    /// non-trivial composites. Compile-pipeline sites that fold a static
    /// sub-expression to a literal use this instead of `value_with_id` so
    /// the prebuilt view exists *before* any enclosing static operator is
    /// itself constant-folded — `evaluate_switch`'s folded-case-table arms
    /// match on `lit: Some` structurally, so a compile-time evaluation of
    /// a fully-static `switch` needs the table's `PreLit` already in
    /// place. Never called at runtime (composite prebuilds clone the
    /// literal); runtime wrappers go through [`Self::synthetic_value`].
    pub(crate) fn compile_time_value(id: NodeId, value: OwnedDataValue) -> Self {
        let mut node = Self::value_with_id(id, value);
        if let CompiledNode::Value { value, lit, .. } = &mut node {
            if lit.is_none() {
                *lit = PreLit::composite(value);
            }
        }
        node
    }

    /// Returns the name of this node's top-level operator, if any.
    ///
    /// Used when wrapping an error with structured context — we only report
    /// the outermost operator, not the full nested call chain. Borrows a
    /// static name for built-ins and the named compiled forms; allocates
    /// only for `CustomOperator` (the user-supplied name). `Logic` caches the
    /// root node's result at compile time (see `Logic::new`).
    pub(crate) fn operator_name(&self) -> Option<std::borrow::Cow<'static, str>> {
        use std::borrow::Cow;
        match self {
            CompiledNode::BuiltinOperator { opcode, .. } => Some(Cow::Borrowed(opcode.as_str())),
            CompiledNode::CustomOperator(data) => Some(Cow::Owned(data.name.clone())),
            CompiledNode::Cse(data) => data.inner.operator_name(),
            CompiledNode::Var { .. } => Some(Cow::Borrowed("var")),
            #[cfg(feature = "ext-control")]
            CompiledNode::Exists(_) => Some(Cow::Borrowed("exists")),
            #[cfg(feature = "error-handling")]
            CompiledNode::Throw(_) => Some(Cow::Borrowed("throw")),
            CompiledNode::Missing(_) => Some(Cow::Borrowed("missing")),
            CompiledNode::MissingSome(_) => Some(Cow::Borrowed("missing_some")),
            CompiledNode::InvalidArgs { op_name, .. } => Some(Cow::Borrowed(op_name)),
            _ => None,
        }
    }
}

#[cfg(test)]
mod layout_tests {
    /// `CompiledNode` is the hot dispatch type — every evaluation walks
    /// slices of it, so its size directly drives cache behaviour. 48
    /// bytes is the current all-features size on 64-bit; a variant that
    /// pushes it past that belongs behind a `Box`.
    #[test]
    #[cfg(target_pointer_width = "64")]
    fn compiled_node_stays_small() {
        assert!(
            std::mem::size_of::<super::CompiledNode>() <= 48,
            "CompiledNode grew past 48 bytes ({}); box the payload of the offending variant",
            std::mem::size_of::<super::CompiledNode>()
        );
    }
}