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
use crate::{Engine, Result};

// =============================================================================
// Arena-mode missing / missing_some
//
// Path lookups walk `&DataValue` natively via `path_exists_*`.
// =============================================================================

use super::eager::Cx;
use super::extract::{Any, RestArgs};
use crate::arena::{ContextStack, DataValue};
use bumpalo::Bump;

/// Resolve the lookup-target for `missing` / `missing_some` — current
/// context's data view as `&'a DataValue<'a>`.
#[inline(always)]
fn lookup_data<'a>(ctx: &ContextStack<'a>) -> &'a DataValue<'a> {
    // No depth probe needed: with nothing pushed, `current()` is
    // `ContextRef::Root(root)` and its `data()` yields the very pointer
    // `root_input()` returns, so the old `depth() > 0` branch could never
    // select a different value.
    ctx.current().data()
}

/// `missing`: the paths (strings, or arrays of them) not present in the
/// current data. Accumulates missing-path strings directly into the arena.
/// The compile hook turns most calls into `CompiledNode::Missing`; this
/// body runs for the rest.
#[inline]
pub(crate) fn missing<'a>(
    cx: &mut Cx<'_, 'a>,
    paths: RestArgs<'a, Any>,
) -> Result<&'a DataValue<'a>> {
    let lookup = lookup_data(cx.ctx);
    let mut missing: Option<bumpalo::collections::Vec<'a, DataValue<'a>>> = None;

    for i in 0..paths.len() {
        let av = paths.get(i, cx)?;
        accumulate_dynamic_missing(av, lookup, &mut missing, paths.len(), cx.ctx, cx.arena)?;
    }
    match missing {
        Some(v) if !v.is_empty() => Ok(cx.alloc(DataValue::Array(v.into_bump_slice()))),
        _ => Ok(crate::arena::singletons::singleton_empty_array()),
    }
}

/// `missing_some`: `[]` when at least `min_av` of the listed paths are
/// present, otherwise the missing ones. The compile hook turns most calls
/// into `CompiledNode::MissingSome`; this body runs for the rest.
#[inline]
pub(crate) fn missing_some<'a>(
    cx: &mut Cx<'_, 'a>,
    min_av: &'a DataValue<'a>,
    paths_av: &'a DataValue<'a>,
) -> Result<&'a DataValue<'a>> {
    let (ctx, arena) = (&mut *cx.ctx, cx.arena);
    let min_present = min_present(min_av);
    let lookup = lookup_data(ctx);
    eval_missing_some_dynamic(paths_av, lookup, min_present, ctx, arena)
}

#[inline]
fn eval_missing_some_dynamic<'a>(
    paths_av: &'a DataValue<'a>,
    lookup: &'a DataValue<'a>,
    min_present: usize,
    ctx: &mut ContextStack<'a>,
    arena: &'a Bump,
) -> Result<&'a DataValue<'a>> {
    let mut missing: Option<bumpalo::collections::Vec<'a, DataValue<'a>>> = None;
    let mut present_count: usize = 0;

    // A path list from data is charged its full length up front, whether or
    // not enough paths turn up present to stop early.
    let short_circuit = match paths_av {
        DataValue::Array(items) => {
            ctx.charge(items.len() as u64)?;
            let cap = items.len();
            items.iter().any(|it| {
                it.as_str().is_some_and(|p| {
                    check_path(
                        p,
                        lookup,
                        &mut missing,
                        cap,
                        &mut present_count,
                        min_present,
                        arena,
                    )
                })
            })
        }
        _ => false,
    };

    if short_circuit || present_count >= min_present {
        return Ok(crate::arena::singletons::singleton_empty_array());
    }
    match missing {
        Some(v) if !v.is_empty() => Ok(arena.alloc(DataValue::Array(v.into_bump_slice()))),
        _ => Ok(crate::arena::singletons::singleton_empty_array()),
    }
}

// =============================================================================
// Pre-compiled missing / missing_some — paths parsed into segments at compile.
// =============================================================================

use crate::node::{
    CompiledMissingArg, CompiledMissingData, CompiledMissingMin, CompiledMissingPaths,
    CompiledMissingSomeData,
};

/// Evaluate a `missing` op whose static literal-string paths have been
/// pre-parsed into segments. Static paths walk via
/// `path_exists_segments`; dynamic args use the runtime path string.
///
/// Defers the per-call bumpalo `Vec` allocation until the first missing
/// path is found. The dominant case in real workloads (and in
/// `compatible.json`) is "all paths present" → return the static
/// empty-array singleton without touching the arena at all.
#[inline]
pub(crate) fn evaluate_compiled_missing<'a>(
    data: &'a CompiledMissingData,
    ctx: &mut ContextStack<'a>,
    engine: &Engine,
    arena: &'a Bump,
) -> Result<&'a DataValue<'a>> {
    let lookup = lookup_data(ctx);
    let mut missing: Option<bumpalo::collections::Vec<'a, DataValue<'a>>> = None;

    for arg in data.args.iter() {
        match arg {
            CompiledMissingArg::Now((path, segments)) => {
                if !crate::arena::value::path_exists_segments(lookup, segments) {
                    missing
                        .get_or_insert_with(|| {
                            bumpalo::collections::Vec::with_capacity_in(data.args.len(), arena)
                        })
                        .push(DataValue::String(path.as_ref()));
                }
            }
            CompiledMissingArg::Later(node) => {
                let av = engine.dispatch_node(node, ctx, arena)?;
                accumulate_dynamic_missing(av, lookup, &mut missing, data.args.len(), ctx, arena)?;
            }
        }
    }
    match missing {
        Some(v) if !v.is_empty() => Ok(arena.alloc(DataValue::Array(v.into_bump_slice()))),
        _ => Ok(crate::arena::singletons::singleton_empty_array()),
    }
}

/// Evaluate a `missing_some` op whose literal min-count and literal array-of-
/// strings paths have been pre-resolved at compile time.
#[inline]
pub(crate) fn evaluate_compiled_missing_some<'a>(
    data: &'a CompiledMissingSomeData,
    ctx: &mut ContextStack<'a>,
    engine: &Engine,
    arena: &'a Bump,
) -> Result<&'a DataValue<'a>> {
    let min_present = match &data.min_present {
        CompiledMissingMin::Now(n) => *n,
        CompiledMissingMin::Later(node) => {
            let av = engine.dispatch_node(node, ctx, arena)?;
            min_present(av)
        }
    };

    let lookup = lookup_data(ctx);

    match &data.paths {
        CompiledMissingPaths::Now(paths) => {
            // Defer the missing-paths buffer until the first path actually
            // misses — the dominant success case ("all paths present"
            // hits min_present early) returns the empty-array singleton
            // without allocating.
            let mut missing: Option<bumpalo::collections::Vec<'a, DataValue<'a>>> = None;
            let mut present = 0usize;
            for (path, segments) in paths.iter() {
                if crate::arena::value::path_exists_segments(lookup, segments) {
                    present += 1;
                    if present >= min_present {
                        return Ok(crate::arena::singletons::singleton_empty_array());
                    }
                } else {
                    missing
                        .get_or_insert_with(|| {
                            bumpalo::collections::Vec::with_capacity_in(paths.len(), arena)
                        })
                        .push(DataValue::String(path.as_ref()));
                }
            }
            match missing {
                Some(v) if present < min_present && !v.is_empty() => {
                    Ok(arena.alloc(DataValue::Array(v.into_bump_slice())))
                }
                _ => Ok(crate::arena::singletons::singleton_empty_array()),
            }
        }
        CompiledMissingPaths::Later(node) => {
            let paths_av = engine.dispatch_node(node, ctx, arena)?;
            eval_missing_some_dynamic(paths_av, lookup, min_present, ctx, arena)
        }
    }
}

/// How many paths `missing_some` needs present: its first argument, rounded
/// up (at least 2.5 present means 3), and never below 0. Anything but a
/// number needs 1.
#[inline]
pub(crate) fn min_present(av: &DataValue<'_>) -> usize {
    match av {
        DataValue::Number(n) => match n.as_i64() {
            // Saturating, not `as`: on a 32-bit target (wasm32) `as`
            // would wrap a minimum past `usize::MAX` to a small one.
            Some(i) => usize::try_from(i.max(0)).unwrap_or(usize::MAX),
            // `as` saturates: a huge minimum is never met, NaN needs none.
            None => n.as_f64().ceil().max(0.0) as usize,
        },
        _ => 1,
    }
}

#[inline]
fn check_path<'a>(
    path: &'a str,
    lookup: &'a DataValue<'a>,
    missing: &mut Option<bumpalo::collections::Vec<'a, DataValue<'a>>>,
    cap: usize,
    present: &mut usize,
    min_present: usize,
    arena: &'a Bump,
) -> bool {
    if !crate::arena::value::path_exists_str(lookup, path) {
        // The path already lives for `'a` (arena or rule data): borrowed.
        missing
            .get_or_insert_with(|| bumpalo::collections::Vec::with_capacity_in(cap, arena))
            .push(DataValue::String(path));
    } else {
        *present += 1;
        if *present >= min_present {
            return true;
        }
    }
    false
}

/// Check a dynamic `missing` argument: one path string, or an array of them.
/// An array is charged one operation per path before any is looked up: its
/// length comes from data, so the node charge alone would not bound it.
#[inline]
fn accumulate_dynamic_missing<'a>(
    av: &'a DataValue<'a>,
    lookup: &'a DataValue<'a>,
    missing: &mut Option<bumpalo::collections::Vec<'a, DataValue<'a>>>,
    cap: usize,
    ctx: &mut ContextStack<'a>,
    arena: &'a Bump,
) -> Result<()> {
    match av {
        DataValue::Array(items) => {
            ctx.charge(items.len() as u64)?;
            for it in *items {
                if let Some(path) = it.as_str()
                    && !crate::arena::value::path_exists_str(lookup, path)
                {
                    missing
                        .get_or_insert_with(|| {
                            bumpalo::collections::Vec::with_capacity_in(cap.max(items.len()), arena)
                        })
                        .push(DataValue::String(path));
                }
            }
        }
        DataValue::String(s) if !crate::arena::value::path_exists_str(lookup, s) => {
            missing
                .get_or_insert_with(|| bumpalo::collections::Vec::with_capacity_in(cap, arena))
                .push(DataValue::String(s));
        }
        _ => {}
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::min_present;
    use crate::arena::DataValue;

    #[test]
    fn a_minimum_past_usize_saturates() {
        let big = DataValue::from_i64(i64::MAX);
        assert_eq!(
            min_present(&big),
            usize::try_from(i64::MAX).unwrap_or(usize::MAX)
        );
        // 2^32 + 1 wrapped to 1 on a 32-bit target.
        assert!(min_present(&DataValue::from_i64((1 << 32) + 1)) > 1);
        assert_eq!(min_present(&DataValue::from_i64(-3)), 0);
        assert_eq!(min_present(&DataValue::from_f64(2.5)), 3);
    }
}