datalogic-rs 5.9.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
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
//! Arena-mode `val` / `var` evaluation.
//!
//! Both source ops normalise to `OpCode::Val`; the var-specific arg shape
//! (path + default fallback) is collapsed at compile time by
//! `compile::operator::try_compile_var`. This module owns the runtime side:
//! the compiled fast path (`evaluate_val_compiled`) and the dynamic-arg form
//! (`evaluate_val`).

use bumpalo::Bump;

use super::{
    CompiledVarSpec, array_get, array_len, current_data, default_or_null, frame_data_at_level,
    level_marker_from_array, metadata_hint_lookup, missed, path_str_from_data, value_path_name,
};
use crate::arena::{ContextStack, DataValue};
use crate::node::{MetadataHint, PathSegment, ReduceHint, ScopeBinding};
use crate::{ArenaExt, CompiledNode, Error, Result};

/// Arena variant of `evaluate_val_compiled`. Dispatches through four
/// resolution stages in order:
///
/// 1. **Metadata** (`{"val": [[n], "index"]}` / `"key"`) — reads the
///    bookkeeping of the frame `metadata_climb(n)` climbs up. Total: a level
///    that names no metadata frame, or a frame with no such slot, is null
///    rather than a field lookup.
/// 2. **Reduce** (`current` / `accumulator` and their `.path` siblings) —
///    reads the reduce frame's slots.
/// 3. **Root-scope fast path** (`scope_level == 0` at root depth) — arena
///    traversal straight from the input, no frame walk. This is the dominant
///    path in real workloads and stays inline for branch-prediction.
/// 4. **General context-stack walk** — for non-root scopes (`{"val": [[1], …]}`).
///
/// Each branch falls through to `default_or_null` on miss; the var's
/// `default_value` (when present) is evaluated lazily there.
#[inline(always)]
pub(crate) fn evaluate_val_compiled<'a>(
    spec: CompiledVarSpec<'a>,
    ctx: &mut ContextStack<'a>,
    engine: &crate::Engine,
    arena: &'a Bump,
) -> Result<&'a DataValue<'a>> {
    let CompiledVarSpec {
        scope_level,
        segments,
        reduce_hint,
        metadata_hint,
        default_value,
        binding,
    } = spec;

    // Cross-validate the compile-time resolution against the runtime walk.
    // Debug builds only; see `debug_check_binding`.
    super::debug_check_binding(binding, scope_level, ctx);

    // Dominant-case fast path: plain root-scope `var` with no metadata/reduce
    // hints, evaluated outside any iteration frame. Probed first as a single
    // combined branch so the common case never pays for the metadata-hint
    // pattern match or the reduce-hint frame inspection below.
    // `binding == Root` is the compile-time form of the old
    // `scope_level == 0 && ctx.depth() == 0` pair, and additionally admits an
    // up-level `val` whose clamp the compiler proved (`[[2]]` inside a single
    // `map`, say). That widening is behaviour-preserving: such a node has no
    // reduce hint and reached `ContextRef::Root` through `get_at_level` before,
    // landing on this same `traverse_segments` / `default_or_null` pair.
    if binding == ScopeBinding::Root
        && metadata_hint == MetadataHint::None
        && reduce_hint == ReduceHint::None
    {
        let root_av = ctx.root_input();
        let resolved = if segments.is_empty() {
            Some(root_av)
        } else {
            crate::arena::value::traverse_segments(root_av, segments)
        };
        return match resolved {
            Some(av) => Ok(av),
            None => default_or_null(default_value, segments, ctx, engine, arena),
        };
    }

    if metadata_hint != MetadataHint::None {
        return Ok(resolve_metadata_hint(
            metadata_hint,
            scope_level as isize,
            ctx,
            arena,
        ));
    }

    if let Some(res) = resolve_reduce_hint(
        reduce_hint,
        binding,
        segments,
        ctx,
        engine,
        arena,
        default_value,
    ) {
        return res;
    }

    resolve_via_context_stack(scope_level, segments, ctx, engine, arena, default_value)
}

/// Stage 1 — metadata hints (`index` / `key`), read from the frame
/// `metadata_climb(level)` climbs above the current one: `[[1]]` is the
/// innermost frame, `[[3]]` the one enclosing it. The compiler only sets a
/// hint on a level that names a metadata frame, so the remaining misses are
/// runtime ones — a climb past the outermost frame, `key` over an array, or a
/// `reduce` / `try` frame, which carry no iteration metadata — and all of them
/// are null. This is **total**: the caller never falls through to a data read
/// of a field literally named `index` or `key`.
///
/// Shared with the interpreted path (`super::metadata_hint_lookup`), which
/// classifies the path string and then calls this, so "what `index` evaluates
/// to" has one definition.
#[inline]
pub(super) fn resolve_metadata_hint<'a>(
    hint: MetadataHint,
    level: isize,
    ctx: &ContextStack<'a>,
    arena: &'a Bump,
) -> &'a DataValue<'a> {
    debug_assert!(
        crate::arena::metadata_climb(level.unsigned_abs()).is_some(),
        "metadata hint on level {level}, which names an element frame - the \
         compile-time parity gate broke"
    );
    let frame = ctx.metadata_at_level(level);
    let resolved = match hint {
        MetadataHint::Index => frame
            .and_then(|f| f.get_index())
            .map(|idx| arena.i64(idx as i64)),
        // `key` already has lifetime `'a` (object pairs live in the arena
        // for the call) — no `alloc_str` copy needed; only the `String`
        // wrapper requires a bump alloc.
        MetadataHint::Key => frame
            .and_then(|f| f.get_key())
            .map(|key| &*arena.alloc(DataValue::String(key))),
        MetadataHint::None => {
            debug_assert!(false, "guarded by the caller");
            None
        }
    };
    resolved.unwrap_or_else(crate::arena::singletons::singleton_null)
}

/// Stage 2 — reduce-frame hints. `Current` / `Accumulator` return the slot
/// directly; `CurrentPath` / `AccumulatorPath` traverse `segments[1..]` on
/// the slot (segments[0] is `current`/`accumulator`). Returns:
/// - `Some(Ok(av))` — slot resolved.
/// - `Some(Err(...))` or `Some(Ok(default))` — frame existed but path missed.
/// - `None` — no reduce frame at the current depth; fall through.
#[inline]
fn resolve_reduce_hint<'a>(
    reduce_hint: ReduceHint,
    binding: ScopeBinding,
    segments: &[PathSegment],
    ctx: &mut ContextStack<'a>,
    engine: &crate::Engine,
    arena: &'a Bump,
    default_value: Option<&'a CompiledNode>,
) -> Option<Result<&'a DataValue<'a>>> {
    // A reduce hint is only ever compiled with `scope_level == 0`, so a `Root`
    // binding here means static depth 0 — no reduce frame can exist.
    if reduce_hint == ReduceHint::None || binding == ScopeBinding::Root {
        return None;
    }
    use crate::arena::context::ContextRef;
    let ContextRef::Frame(f) = ctx.current() else {
        return None;
    };

    match reduce_hint {
        ReduceHint::Current => f.get_reduce_current().map(Ok),
        ReduceHint::Accumulator => f.get_reduce_accumulator().map(Ok),
        ReduceHint::CurrentPath | ReduceHint::AccumulatorPath => {
            let slot = if reduce_hint == ReduceHint::CurrentPath {
                f.get_reduce_current()
            } else {
                f.get_reduce_accumulator()
            };
            // Slot must exist for the frame to be considered a reduce frame.
            // If the path traversal misses, return the var's `default_value`.
            let slot = slot?;
            let resolved = crate::arena::value::traverse_segments(slot, &segments[1..]);
            Some(match resolved {
                Some(av) => Ok(av),
                None => default_or_null(default_value, segments, ctx, engine, arena),
            })
        }
        ReduceHint::None => unreachable!(),
    }
}

/// Stage 4 — generic context-stack walk for non-root scopes. `scope_level`
/// of 0 at non-root depth reads the current frame; positive levels walk up.
#[inline]
fn resolve_via_context_stack<'a>(
    scope_level: u32,
    segments: &[PathSegment],
    ctx: &mut ContextStack<'a>,
    engine: &crate::Engine,
    arena: &'a Bump,
    default_value: Option<&'a CompiledNode>,
) -> Result<&'a DataValue<'a>> {
    let aref = if scope_level == 0 {
        ctx.current()
    } else {
        ctx.get_at_level(scope_level as isize)
            .ok_or_else(|| Error::invalid_context_level(scope_level as isize))?
    };
    let av = aref.data();
    if segments.is_empty() {
        return Ok(av);
    }
    match crate::arena::value::traverse_segments(av, segments) {
        Some(child) => Ok(child),
        None => default_or_null(default_value, segments, ctx, engine, arena),
    }
}

/// Arena-native `val` operator. Mirrors the value-mode shape (level access,
/// path chains, reduce shortcuts) but stays on `&DataValue` throughout.
///
/// Three branches:
/// - **Empty args** → current frame data.
/// - **Multi-arg** ([`eval_val_multiarg`]) — distinguishes `[[level], …]`
///   from path-chain at runtime by looking at the first arg.
/// - **Single arg** — null/array/scalar dispatch via
///   [`eval_val_array_path`] and [`eval_val_scalar_path`].
///
/// Also handles the dynamic-fallback `var` shape (path + default) — when
/// `var`'s args[0] is a non-array path that resolves to null and args[1]
/// exists, args[1] is evaluated as the default. The path-chain
/// interpretation (`{"val": ["a", "b"]}` walks `a.b`) is unaffected because
/// chain-walking runs ahead of the fallback check.
#[inline]
pub(crate) fn evaluate_val<'a>(
    args: &'a [CompiledNode],
    ctx: &mut ContextStack<'a>,
    engine: &crate::Engine,
    arena: &'a Bump,
) -> Result<&'a DataValue<'a>> {
    if args.is_empty() {
        return Ok(current_data(ctx));
    }
    if args.len() >= 2 {
        return eval_val_multiarg(args, ctx, engine, arena);
    }
    let path_av = engine.dispatch_node(&args[0], ctx, arena)?;
    if matches!(path_av, DataValue::Null) {
        return Ok(current_data(ctx));
    }
    let found = match array_len(path_av) {
        Some(arr_len) => lookup_array_path(path_av, arr_len, ctx, arena)?,
        None => lookup_scalar_path(path_av, ctx),
    };
    match found {
        Some(av) => Ok(av),
        None => missed(engine, || value_path_name(path_av)),
    }
}

/// Multi-arg `val` form (`args.len() >= 2`). Evaluates `args[0]` once and
/// branches on whether it is a `[level]` marker:
/// - `[[level], path...]` — frame walk at relative level, optional metadata
///   short-circuit when there are exactly 2 args.
/// - Otherwise — path chain on current data, with reduce shortcut for
///   the first segment (`current` / `accumulator`).
fn eval_val_multiarg<'a>(
    args: &'a [CompiledNode],
    ctx: &mut ContextStack<'a>,
    engine: &crate::Engine,
    arena: &'a Bump,
) -> Result<&'a DataValue<'a>> {
    use crate::arena::context::ContextRef;
    use crate::arena::value::{access_path_str_ref, apply_path_element};

    let first_av = engine.dispatch_node(&args[0], ctx, arena)?;
    if let Some(level) = level_marker_from_array(first_av) {
        // Metadata short-circuits — only valid with exactly 2 args.
        if args.len() == 2 {
            let path_av = engine.dispatch_node(&args[1], ctx, arena)?;
            let path_str = path_av.as_str().unwrap_or("");
            if let Some(av) = metadata_hint_lookup(ctx, level, path_str, arena) {
                return Ok(av);
            }

            let path_str = path_str_from_data(path_av, arena);
            let frame_data = frame_data_at_level(ctx, level as isize)
                .ok_or_else(|| Error::invalid_context_level(level as isize))?;
            return match access_path_str_ref(frame_data, path_str) {
                Some(av) => Ok(av),
                None => missed(engine, || path_str.to_string()),
            };
        }

        // Multi-arg path chain at a relative level.
        let mut paths: bumpalo::collections::Vec<'a, &'a str> =
            bumpalo::collections::Vec::with_capacity_in(args.len() - 1, arena);
        for item in args.iter().skip(1) {
            let av = engine.dispatch_node(item, ctx, arena)?;
            paths.push(path_str_from_data(av, arena));
        }
        let mut cur = frame_data_at_level(ctx, level as isize)
            .ok_or_else(|| Error::invalid_context_level(level as isize))?;
        for path in paths.iter() {
            match access_path_str_ref(cur, path) {
                Some(next) => cur = next,
                None => return missed(engine, || paths.join(".")),
            }
        }
        return Ok(cur);
    }

    // Non-level multi-arg path chain: pre-eval all args.
    let mut evaluated: bumpalo::collections::Vec<'a, &'a DataValue<'a>> =
        bumpalo::collections::Vec::with_capacity_in(args.len(), arena);
    evaluated.push(first_av);
    for arg in args.iter().skip(1) {
        evaluated.push(engine.dispatch_node(arg, ctx, arena)?);
    }

    // Reduce shortcut for the first segment.
    let mut start: Option<&'a DataValue<'a>> = None;
    if let ContextRef::Frame(frame) = ctx.current()
        && let Some(s) = evaluated[0].as_str()
    {
        start = if s == "current" {
            frame.get_reduce_current()
        } else if s == "accumulator" {
            frame.get_reduce_accumulator()
        } else {
            None
        };
    }

    let (mut cur, rest_start) = match start {
        Some(s) => (s, 1),
        None => (current_data(ctx), 0),
    };
    for elem in &evaluated[rest_start..] {
        match apply_path_element(cur, elem) {
            Some(next) => cur = next,
            None => {
                return missed(engine, || {
                    let parts: Vec<String> = evaluated.iter().map(|v| value_path_name(v)).collect();
                    parts.join(".")
                });
            }
        }
    }
    Ok(cur)
}

/// Path arg that evaluated to an array. Distinguishes
/// `[[level], path...]` from a plain path-chain array. Empty array →
/// current data (matches `{"var": []}` semantics).
///
/// The bare `{"val": [[N]]}` is not handled here: its level is a literal by
/// construction, so `try_compile_val` always turns it into a `Var` node. A
/// one-element array reaching this path is therefore a *computed* path, and
/// keeps its path meaning — `[0]` is "index 0", not "level 0".
///
/// `None` when the path misses, so `var` can tell a miss from a present
/// `null`; [`eval_val_array_path`] reads a miss as `null`.
fn lookup_array_path<'a>(
    path_av: &'a DataValue<'a>,
    arr_len: usize,
    ctx: &mut ContextStack<'a>,
    arena: &'a Bump,
) -> Result<Option<&'a DataValue<'a>>> {
    use crate::arena::value::apply_path_element;

    if arr_len == 0 {
        return Ok(Some(current_data(ctx)));
    }
    if arr_len >= 2 {
        let level_opt = array_get(path_av, 0).and_then(level_marker_from_array);
        if let Some(level) = level_opt {
            if arr_len == 2 {
                let second = array_get(path_av, 1)
                    .unwrap_or_else(|| crate::arena::singletons::singleton_null());
                let path_str = second.as_str().unwrap_or("");
                if let Some(av) = metadata_hint_lookup(ctx, level, path_str, arena) {
                    return Ok(Some(av));
                }
            }

            let mut cur = frame_data_at_level(ctx, level as isize)
                .ok_or_else(|| Error::invalid_context_level(level as isize))?;
            for i in 1..arr_len {
                let item = array_get(path_av, i)
                    .unwrap_or_else(|| crate::arena::singletons::singleton_null());
                match apply_path_element(cur, item) {
                    Some(next) => cur = next,
                    None => return Ok(None),
                }
            }
            return Ok(Some(cur));
        }
    }

    // Plain path-chain array.
    let mut cur = current_data(ctx);
    for i in 0..arr_len {
        let elem =
            array_get(path_av, i).unwrap_or_else(|| crate::arena::singletons::singleton_null());
        match apply_path_element(cur, elem) {
            Some(next) => cur = next,
            None => return Ok(None),
        }
    }
    Ok(Some(cur))
}

/// Path arg that is a string or numeric scalar.
/// Strings get the reduce-shortcut probe (`current` / `accumulator` /
/// dotted siblings) and the "direct key wins over dotted-path" rule;
/// non-negative integers index a numeric key on current data.
///
/// `None` when the path misses (any other scalar always misses), so `var`
/// can tell a miss from a present `null`; [`eval_val_scalar_path`] reads a
/// miss as `null`.
fn lookup_scalar_path<'a>(
    path_av: &'a DataValue<'a>,
    ctx: &ContextStack<'a>,
) -> Option<&'a DataValue<'a>> {
    use crate::arena::context::ContextRef;
    use crate::arena::value::access_path_str_ref;

    if let Some(s) = path_av.as_str() {
        if let ContextRef::Frame(frame) = ctx.current() {
            if s == "current" {
                if let Some(av) = frame.get_reduce_current() {
                    return Some(av);
                }
            } else if s == "accumulator" {
                if let Some(av) = frame.get_reduce_accumulator() {
                    return Some(av);
                }
            } else if let Some(rest) = s.strip_prefix("current.") {
                if let Some(cur) = frame.get_reduce_current() {
                    return access_path_str_ref(cur, rest);
                }
            } else if let Some(rest) = s.strip_prefix("accumulator.")
                && let Some(acc) = frame.get_reduce_accumulator()
            {
                return access_path_str_ref(acc, rest);
            }
        }

        let cur = current_data(ctx);
        // Direct object key lookup beats dot-path traversal so empty keys and
        // keys containing dots resolve correctly.
        if let DataValue::Object(pairs) = cur
            && let Some(av) = crate::arena::value::object_lookup_field(pairs, s)
        {
            return Some(av);
        }
        return access_path_str_ref(cur, s);
    }

    if let Some(i) = path_av.as_i64()
        && i >= 0
    {
        let cur = current_data(ctx);
        // Common small indices (0..100) hit the static `&'static str`
        // cache; larger keys format into a stack buffer with `itoa`.
        if let Some(static_key) = super::small_int_str(i) {
            return access_path_str_ref(cur, static_key);
        }
        let mut buf = itoa::Buffer::new();
        return access_path_str_ref(cur, buf.format(i));
    }

    None
}

/// `var` with a path that is not a string or number literal (computed, or
/// an array literal) and a default: `{"var": [path, default]}`. Resolves the
/// path the way a one-argument `var` / `val` does and takes `default` only
/// when the path misses, matching a literal-path `var`: a present `null`,
/// `0`, `""` or `false` is returned as is. A `null` path reads the whole
/// current data. Arguments after the default are ignored.
///
/// Before this existed the shape fell through to [`evaluate_val`], which
/// reads every argument as a path segment, so the default became a second
/// segment and the result was almost always `null`.
pub(crate) fn evaluate_var_default<'a>(
    args: &'a [CompiledNode],
    ctx: &mut ContextStack<'a>,
    engine: &crate::Engine,
    arena: &'a Bump,
) -> Result<&'a DataValue<'a>> {
    let Some(path_node) = args.first() else {
        return Ok(current_data(ctx));
    };
    let path_av = engine.dispatch_node(path_node, ctx, arena)?;
    let found = if matches!(path_av, DataValue::Null) {
        Some(current_data(ctx))
    } else if let Some(arr_len) = array_len(path_av) {
        lookup_array_path(path_av, arr_len, ctx, arena)?
    } else {
        lookup_scalar_path(path_av, ctx)
    };
    match (found, args.get(1)) {
        (Some(av), _) => Ok(av),
        (None, Some(default)) => engine.dispatch_node(default, ctx, arena),
        (None, None) => missed(engine, || value_path_name(path_av)),
    }
}