Skip to main content

jsonata_core/
lib.rs

1// jsonatapy - High-performance Python implementation of JSONata
2// Copyright (c) 2025 jsonatapy contributors
3// Licensed under the MIT License
4
5//! # jsonatapy
6//!
7//! A high-performance Rust implementation of JSONata - the JSON query and
8//! transformation language - with optional Python bindings via PyO3.
9//!
10//! ## Rust API
11//!
12//! ```rust,ignore
13//! use jsonata_core::parser;
14//! use jsonata_core::evaluator::Evaluator;
15//! use jsonata_core::value::JValue;
16//!
17//! let ast = parser::parse("user.name").unwrap();
18//! let data = JValue::from_json_str(r#"{"user":{"name":"Alice"}}"#).unwrap();
19//! let result = Evaluator::new().evaluate(&ast, &data).unwrap();
20//! ```
21//!
22//! ## Architecture
23//!
24//! - `parser` - Expression parser (converts JSONata strings to AST)
25//! - `evaluator` - Expression evaluator (executes AST against data)
26//! - `functions` - Built-in function implementations
27//! - `datetime` - Date/time handling functions
28//! - `signature` - Function signature validation
29//! - `ast` - Abstract Syntax Tree definitions
30//! - `value` - JValue type (the runtime value representation)
31
32pub mod ast;
33pub mod ast_transform;
34#[cfg(feature = "capi")]
35pub mod capi;
36mod compiler;
37mod datetime;
38pub mod evaluator;
39pub mod functions;
40#[cfg(feature = "python")]
41pub mod lazy;
42pub mod parser;
43mod signature;
44pub mod value;
45mod vm;
46
47// ── Benchmarking facade (only when the "bench" feature is enabled) ────────────
48//
49// Exposes the compiler/VM pipeline for Criterion benchmarks without making
50// the internals part of the permanent public API.
51
52/// Internal benchmarking API — do not use in production code.
53///
54/// Enabled with `--features bench`. Provides access to the bytecode compiler
55/// and VM so that Criterion benchmarks can measure tree-walker vs VM directly.
56#[cfg(feature = "bench")]
57pub mod _bench {
58    use crate::ast::AstNode;
59    pub use crate::evaluator::EvaluatorError;
60    use crate::value::JValue;
61
62    /// An opaque handle to a compiled bytecode program.
63    pub struct CompiledProgram(crate::vm::BytecodeProgram);
64
65    /// Compile an AST node to bytecode.
66    ///
67    /// Returns `None` if the expression contains constructs the compiler
68    /// doesn't handle (e.g. wildcards, `$eval`, higher-order functions at
69    /// the top level). In that case, fall back to `Evaluator::new().evaluate()`.
70    pub fn compile(ast: &AstNode) -> Option<CompiledProgram> {
71        crate::evaluator::try_compile_expr(ast)
72            .map(|ce| CompiledProgram(crate::compiler::BytecodeCompiler::compile(&ce)))
73    }
74
75    /// Execute a compiled program against `data`.
76    pub fn run(prog: &CompiledProgram, data: &JValue) -> Result<JValue, EvaluatorError> {
77        crate::vm::Vm::with_options(&prog.0, crate::evaluator::EvaluatorOptions::default())
78            .run(data, None)
79    }
80}
81
82// ── Python bindings (only when the "python" feature is enabled) ───────────────
83
84/// The JSONata reference implementation version this library targets.
85const JSONATA_REFERENCE_VERSION: &str = "2.1.0";
86
87#[cfg(feature = "python")]
88use crate::value::JValue;
89#[cfg(feature = "python")]
90use pyo3::exceptions::{PyTypeError, PyValueError};
91#[cfg(feature = "python")]
92use pyo3::prelude::*;
93#[cfg(feature = "python")]
94use pyo3::types::{PyDict, PyList, PyString};
95
96/// Pre-converted data handle for efficient repeated evaluation.
97///
98/// Convert Python data to an internal representation once, then reuse it
99/// across multiple evaluations to avoid repeated Python↔Rust conversion overhead.
100///
101/// # Examples
102///
103/// ```python
104/// import jsonatapy
105///
106/// data = jsonatapy.JsonataData({"orders": [{"price": 150}, {"price": 50}]})
107/// expr = jsonatapy.compile("orders[price > 100]")
108/// result = expr.evaluate_with_data(data)
109/// ```
110#[cfg(feature = "python")]
111#[pyclass(unsendable)]
112struct JsonataData {
113    data: JValue,
114}
115
116#[cfg(feature = "python")]
117#[pymethods]
118impl JsonataData {
119    /// Create from a Python object (dict, list, etc.)
120    #[new]
121    fn new(py: Python, data: Py<PyAny>) -> PyResult<Self> {
122        let jvalue = python_to_json(py, &data)?;
123        Ok(JsonataData { data: jvalue })
124    }
125
126    /// Create from a JSON string (fastest path).
127    #[staticmethod]
128    fn from_json(json_str: &str) -> PyResult<Self> {
129        let data = JValue::from_json_str(json_str)
130            .map_err(|e| PyValueError::new_err(format!("Invalid JSON: {}", e)))?;
131        Ok(JsonataData { data })
132    }
133}
134
135/// A compiled JSONata expression that can be evaluated against data.
136///
137/// This is the main entry point for using JSONata. Compile an expression once,
138/// then evaluate it multiple times against different data.
139///
140/// # Examples
141///
142/// ```python
143/// import jsonatapy
144///
145/// # Compile once
146/// expr = jsonatapy.compile("orders[price > 100].product")
147///
148/// # Evaluate many times
149/// data1 = {"orders": [{"product": "A", "price": 150}]}
150/// result1 = expr.evaluate(data1)
151///
152/// data2 = {"orders": [{"product": "B", "price": 50}]}
153/// result2 = expr.evaluate(data2)
154/// ```
155#[cfg(feature = "python")]
156#[pyclass(unsendable)]
157struct JsonataExpression {
158    /// The parsed Abstract Syntax Tree
159    ast: ast::AstNode,
160    /// Lazily compiled bytecode — populated on first evaluate() call.
161    /// `Some(bc)` = fast VM path; `None` = must use tree-walker.
162    /// `OnceCell` ensures compilation happens at most once per expression instance.
163    bytecode: std::cell::OnceCell<Option<vm::BytecodeProgram>>,
164    /// Default guardrail options set at `compile()` time. Per-call `evaluate*()`
165    /// kwargs override these on a field-by-field basis (see the `.or(...)` merges
166    /// in the `#[pymethods]` impl below).
167    default_options: evaluator::EvaluatorOptions,
168}
169
170/// Test-support toggle: bypass the bytecode VM and exercise the tree-walking
171/// evaluator on every call. Seeded once at module import from the
172/// JSONATAPY_FORCE_TREE_WALKER env var (whole-process forcing, e.g. the CI
173/// tree-walker reference-suite job), and flippable at runtime through the
174/// private `_set_force_tree_walker` pyfunction (what the Python tests use).
175///
176/// This was previously a per-call `env::var_os` read (~100-200ns), which is
177/// NOT noise next to a sub-microsecond evaluation: it showed up as a 10-30%
178/// regression on tiny expressions in v2.2.4 (issue #74). A relaxed atomic
179/// load is ~1ns and preserves the flip-mid-test capability.
180#[cfg(feature = "python")]
181static FORCE_TREE_WALKER: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
182
183#[cfg(feature = "python")]
184fn force_tree_walker() -> bool {
185    FORCE_TREE_WALKER.load(std::sync::atomic::Ordering::Relaxed)
186}
187
188/// Private test hook: force (or unforce) the tree-walking evaluator for all
189/// subsequent evaluations in this process. Not part of the public API.
190#[cfg(feature = "python")]
191#[pyfunction]
192fn _set_force_tree_walker(on: bool) {
193    FORCE_TREE_WALKER.store(on, std::sync::atomic::Ordering::Relaxed);
194}
195
196/// Private test hook: current state of the tree-walker toggle.
197#[cfg(feature = "python")]
198#[pyfunction]
199fn _get_force_tree_walker() -> bool {
200    force_tree_walker()
201}
202
203#[cfg(feature = "python")]
204impl JsonataExpression {
205    /// Evaluate the compiled expression against pre-converted data.
206    /// Uses bytecode VM when available, falls back to tree-walker.
207    fn run_eval(
208        &self,
209        py: Python,
210        data: &JValue,
211        bindings: Option<Py<PyAny>>,
212        options: evaluator::EvaluatorOptions,
213    ) -> PyResult<JValue> {
214        if bindings.is_none() && !force_tree_walker() {
215            let bytecode = self.bytecode.get_or_init(|| {
216                evaluator::try_compile_expr(&self.ast)
217                    .map(|ce| compiler::BytecodeCompiler::compile(&ce))
218            });
219            if let Some(bc) = bytecode {
220                vm::Vm::with_options(bc, options.clone())
221                    .run(data, None)
222                    .map_err(evaluator_error_to_py)
223            } else {
224                let mut ev = evaluator::Evaluator::with_options(evaluator::Context::new(), options);
225                ev.evaluate(&self.ast, data).map_err(evaluator_error_to_py)
226            }
227        } else {
228            let mut ev = create_evaluator(py, bindings, options)?;
229            ev.evaluate(&self.ast, data).map_err(evaluator_error_to_py)
230        }
231    }
232}
233
234#[cfg(feature = "python")]
235#[pymethods]
236impl JsonataExpression {
237    /// Returns ValueError if evaluation fails
238    #[pyo3(signature = (data, bindings=None, timeout=None, max_stack_depth=None, max_sequence_length=None))]
239    #[allow(clippy::too_many_arguments)]
240    fn evaluate(
241        &self,
242        py: Python,
243        data: Py<PyAny>,
244        bindings: Option<Py<PyAny>>,
245        timeout: Option<u64>,
246        max_stack_depth: Option<usize>,
247        max_sequence_length: Option<usize>,
248    ) -> PyResult<Py<PyAny>> {
249        let json_data = lazy::convert(data.bind(py), true)?;
250        let options = evaluator::EvaluatorOptions {
251            timeout_ms: timeout.or(self.default_options.timeout_ms),
252            max_stack_depth: max_stack_depth.or(self.default_options.max_stack_depth),
253            max_sequence_length: max_sequence_length.or(self.default_options.max_sequence_length),
254        };
255        json_to_python(py, &self.run_eval(py, &json_data, bindings, options)?)
256    }
257
258    /// Evaluate with a pre-converted data handle (fastest for repeated evaluation).
259    ///
260    /// # Arguments
261    ///
262    /// * `data` - A JsonataData handle (pre-converted from Python to internal format)
263    /// * `bindings` - Optional additional variable bindings
264    ///
265    /// # Returns
266    ///
267    /// The result of evaluating the expression
268    #[pyo3(signature = (data, bindings=None, timeout=None, max_stack_depth=None, max_sequence_length=None))]
269    #[allow(clippy::too_many_arguments)]
270    fn evaluate_with_data(
271        &self,
272        py: Python,
273        data: &JsonataData,
274        bindings: Option<Py<PyAny>>,
275        timeout: Option<u64>,
276        max_stack_depth: Option<usize>,
277        max_sequence_length: Option<usize>,
278    ) -> PyResult<Py<PyAny>> {
279        let options = evaluator::EvaluatorOptions {
280            timeout_ms: timeout.or(self.default_options.timeout_ms),
281            max_stack_depth: max_stack_depth.or(self.default_options.max_stack_depth),
282            max_sequence_length: max_sequence_length.or(self.default_options.max_sequence_length),
283        };
284        json_to_python(py, &self.run_eval(py, &data.data, bindings, options)?)
285    }
286
287    /// Evaluate with a pre-converted data handle, return JSON string (zero-overhead output).
288    ///
289    /// # Arguments
290    ///
291    /// * `data` - A JsonataData handle (pre-converted from Python to internal format)
292    /// * `bindings` - Optional additional variable bindings
293    ///
294    /// # Returns
295    ///
296    /// The result as a JSON string
297    #[pyo3(signature = (data, bindings=None, timeout=None, max_stack_depth=None, max_sequence_length=None))]
298    #[allow(clippy::too_many_arguments)]
299    fn evaluate_data_to_json(
300        &self,
301        py: Python,
302        data: &JsonataData,
303        bindings: Option<Py<PyAny>>,
304        timeout: Option<u64>,
305        max_stack_depth: Option<usize>,
306        max_sequence_length: Option<usize>,
307    ) -> PyResult<String> {
308        let options = evaluator::EvaluatorOptions {
309            timeout_ms: timeout.or(self.default_options.timeout_ms),
310            max_stack_depth: max_stack_depth.or(self.default_options.max_stack_depth),
311            max_sequence_length: max_sequence_length.or(self.default_options.max_sequence_length),
312        };
313        self.run_eval(py, &data.data, bindings, options)?
314            .to_json_string()
315            .map_err(|e| PyValueError::new_err(format!("Failed to serialize result: {}", e)))
316    }
317
318    /// Evaluate the expression with JSON string input/output (faster for large data).
319    ///
320    /// This method avoids Python↔Rust conversion overhead by accepting and returning
321    /// JSON strings directly. This is significantly faster for large datasets.
322    ///
323    /// # Arguments
324    ///
325    /// * `json_str` - Input data as a JSON string
326    /// * `bindings` - Optional dict of variable bindings (default: None)
327    ///
328    /// # Returns
329    ///
330    /// The result as a JSON string
331    ///
332    /// # Errors
333    ///
334    /// Returns ValueError if JSON parsing or evaluation fails
335    #[pyo3(signature = (json_str, bindings=None, timeout=None, max_stack_depth=None, max_sequence_length=None))]
336    #[allow(clippy::too_many_arguments)]
337    fn evaluate_json(
338        &self,
339        py: Python,
340        json_str: &str,
341        bindings: Option<Py<PyAny>>,
342        timeout: Option<u64>,
343        max_stack_depth: Option<usize>,
344        max_sequence_length: Option<usize>,
345    ) -> PyResult<String> {
346        let json_data = JValue::from_json_str(json_str)
347            .map_err(|e| PyValueError::new_err(format!("Invalid JSON: {}", e)))?;
348        let options = evaluator::EvaluatorOptions {
349            timeout_ms: timeout.or(self.default_options.timeout_ms),
350            max_stack_depth: max_stack_depth.or(self.default_options.max_stack_depth),
351            max_sequence_length: max_sequence_length.or(self.default_options.max_sequence_length),
352        };
353        self.run_eval(py, &json_data, bindings, options)?
354            .to_json_string()
355            .map_err(|e| PyValueError::new_err(format!("Failed to serialize result: {}", e)))
356    }
357
358    /// Evaluate with JSON string input, distinguishing an Undefined result
359    /// (returns Python None) from an explicit JSON null result (returns
360    /// the string "null"). evaluate_json() cannot make this distinction --
361    /// JSON serialization has no way to represent "undefined" separately
362    /// from "null" -- so this method checks the raw evaluated JValue's
363    /// is_undefined() BEFORE serializing, exposing the same signal the
364    /// Rust CLI (src/bin/jsonata/main.rs) already uses internally.
365    ///
366    /// `json_str=None` means no input document at all -- the top-level
367    /// context (`$`) is bound to a true `Undefined`, matching the Rust
368    /// CLI's `--null-input` behavior. This is distinct from passing the
369    /// text `"null"`, which binds `$` to an explicit JSON null.
370    ///
371    /// # Errors
372    ///
373    /// Returns ValueError if JSON parsing or evaluation fails
374    #[pyo3(signature = (json_str, bindings=None, timeout=None, max_stack_depth=None, max_sequence_length=None))]
375    #[allow(clippy::too_many_arguments)]
376    fn evaluate_json_or_none(
377        &self,
378        py: Python,
379        json_str: Option<&str>,
380        bindings: Option<Py<PyAny>>,
381        timeout: Option<u64>,
382        max_stack_depth: Option<usize>,
383        max_sequence_length: Option<usize>,
384    ) -> PyResult<Option<String>> {
385        let json_data = match json_str {
386            Some(s) => JValue::from_json_str(s)
387                .map_err(|e| PyValueError::new_err(format!("Invalid JSON: {}", e)))?,
388            None => JValue::Undefined,
389        };
390        let options = evaluator::EvaluatorOptions {
391            timeout_ms: timeout.or(self.default_options.timeout_ms),
392            max_stack_depth: max_stack_depth.or(self.default_options.max_stack_depth),
393            max_sequence_length: max_sequence_length.or(self.default_options.max_sequence_length),
394        };
395        let result = self.run_eval(py, &json_data, bindings, options)?;
396        if result.is_undefined() {
397            return Ok(None);
398        }
399        result
400            .to_json_string()
401            .map(Some)
402            .map_err(|e| PyValueError::new_err(format!("Failed to serialize result: {}", e)))
403    }
404}
405
406/// Compile a JSONata expression into an executable form.
407///
408/// # Arguments
409///
410/// * `expression` - A JSONata query/transformation expression string
411///
412/// # Returns
413///
414/// A compiled JsonataExpression that can be evaluated
415///
416/// # Errors
417///
418/// Returns ValueError if the expression cannot be parsed
419///
420/// # Examples
421///
422/// ```python
423/// import jsonatapy
424///
425/// expr = jsonatapy.compile("$.name")
426/// result = expr.evaluate({"name": "Alice"})
427/// print(result)  # "Alice"
428/// ```
429#[cfg(feature = "python")]
430#[pyfunction]
431#[pyo3(signature = (expression, timeout=None, max_stack_depth=None, max_sequence_length=None))]
432fn compile(
433    expression: &str,
434    timeout: Option<u64>,
435    max_stack_depth: Option<usize>,
436    max_sequence_length: Option<usize>,
437) -> PyResult<JsonataExpression> {
438    let ast = parser::parse(expression).map_err(parser_error_to_py)?;
439
440    Ok(JsonataExpression {
441        ast,
442        bytecode: std::cell::OnceCell::new(),
443        default_options: build_evaluator_options(timeout, max_stack_depth, max_sequence_length),
444    })
445}
446
447/// Evaluate a JSONata expression against data in one step.
448///
449/// This is a convenience function that compiles and evaluates in one call.
450/// For repeated evaluations of the same expression, use `compile()` instead.
451///
452/// # Arguments
453///
454/// * `expression` - A JSONata query/transformation expression string
455/// * `data` - A Python object (typically dict) to query/transform
456/// * `bindings` - Optional additional variable bindings
457///
458/// # Returns
459///
460/// The result of evaluating the expression
461///
462/// # Errors
463///
464/// Returns ValueError if parsing or evaluation fails
465///
466/// # Examples
467///
468/// ```python
469/// import jsonatapy
470///
471/// result = jsonatapy.evaluate("$uppercase(name)", {"name": "alice"})
472/// print(result)  # "ALICE"
473/// ```
474#[cfg(feature = "python")]
475#[pyfunction]
476#[pyo3(signature = (expression, data, bindings=None, timeout=None, max_stack_depth=None, max_sequence_length=None))]
477#[allow(clippy::too_many_arguments)]
478fn evaluate(
479    py: Python,
480    expression: &str,
481    data: Py<PyAny>,
482    bindings: Option<Py<PyAny>>,
483    timeout: Option<u64>,
484    max_stack_depth: Option<usize>,
485    max_sequence_length: Option<usize>,
486) -> PyResult<Py<PyAny>> {
487    let expr = compile(expression, None, None, None)?;
488    expr.evaluate(
489        py,
490        data,
491        bindings,
492        timeout,
493        max_stack_depth,
494        max_sequence_length,
495    )
496}
497
498/// Convert a Python object to a JValue.
499///
500/// Handles conversion of Python types:
501/// - None -> Null
502/// - bool -> Bool (checked before int since bool is a subclass of int)
503/// - int, float -> Number
504/// - str -> String
505/// - list -> Array
506/// - dict -> Object
507#[cfg(feature = "python")]
508fn python_to_json(py: Python, obj: &Py<PyAny>) -> PyResult<JValue> {
509    python_to_json_bound(obj.bind(py))
510}
511
512/// Inner conversion using the Bound API. Delegates to `lazy::convert` with
513/// `lazy=false` for today's eager, fully-materialized conversion (see
514/// `src/lazy.rs` for the zero-overhead type-check details and the lazy path).
515#[cfg(feature = "python")]
516fn python_to_json_bound(obj: &Bound<'_, PyAny>) -> PyResult<JValue> {
517    lazy::convert(obj, false)
518}
519
520/// Convert a JValue to a Python object.
521///
522/// Handles conversion of JValue variants to Python types:
523/// - Null/Undefined -> None
524/// - Bool -> bool
525/// - Number -> int (if whole number) or float
526/// - String -> str
527/// - Array -> list (batch-constructed via PyList::new for fewer C API calls)
528/// - Object -> dict
529/// - Lambda/Builtin/Regex -> None
530#[cfg(feature = "python")]
531fn json_to_python(py: Python, value: &JValue) -> PyResult<Py<PyAny>> {
532    match value {
533        JValue::Null | JValue::Undefined => Ok(py.None()),
534
535        JValue::Bool(b) => Ok(b.into_pyobject(py).unwrap().to_owned().into_any().unbind()),
536
537        JValue::Number(n) => {
538            // If it's a whole number that fits in i64, return as Python int
539            if n.fract() == 0.0 && n.is_finite() && *n >= i64::MIN as f64 && *n <= i64::MAX as f64 {
540                Ok((*n as i64).into_pyobject(py).unwrap().into_any().unbind())
541            } else {
542                Ok(n.into_pyobject(py).unwrap().into_any().unbind())
543            }
544        }
545
546        JValue::String(s) => Ok((&**s).into_pyobject(py).unwrap().into_any().unbind()),
547
548        JValue::Array(arr) => {
549            // Array of objects with shared keys: intern first object's keys as
550            // Python strings to avoid repeated UTF-8 -> PyString conversion.
551            let all_objects =
552                arr.len() >= 2 && arr.iter().all(|item| matches!(item, JValue::Object(_)));
553            if all_objects {
554                let first_obj = match arr.first() {
555                    Some(JValue::Object(obj)) => obj,
556                    _ => unreachable!("all_objects guard ensures first element is an object"),
557                };
558
559                // Intern keys: store (&str, Py<PyString>) — no String clone needed
560                // since first_obj borrows from arr which outlives this block
561                let interned_keys: Vec<(&str, Py<PyString>)> = first_obj
562                    .keys()
563                    .map(|k| (k.as_str(), PyString::new(py, k).unbind()))
564                    .collect();
565
566                let items: Vec<Py<PyAny>> = arr
567                    .iter()
568                    .map(|item| {
569                        // Safe to unwrap: all_objects guarantees every element is Object
570                        let obj = match item {
571                            JValue::Object(obj) => obj,
572                            _ => unreachable!(),
573                        };
574                        let dict = PyDict::new(py);
575                        for (key_str, py_key) in &interned_keys {
576                            if let Some(value) = obj.get(*key_str) {
577                                dict.set_item(py_key.bind(py), json_to_python(py, value)?)?;
578                            }
579                        }
580                        // Handle any extra keys not in first object
581                        for (key, value) in obj.iter() {
582                            if !first_obj.contains_key(key) {
583                                dict.set_item(key, json_to_python(py, value)?)?;
584                            }
585                        }
586                        Ok(dict.unbind().into())
587                    })
588                    .collect::<PyResult<Vec<_>>>()?;
589                return Ok(PyList::new(py, &items)?.unbind().into());
590            }
591
592            // General array: batch construction
593            let items: Vec<Py<PyAny>> = arr
594                .iter()
595                .map(|item| json_to_python(py, item))
596                .collect::<PyResult<Vec<_>>>()?;
597            Ok(PyList::new(py, &items)?.unbind().into())
598        }
599
600        JValue::Object(obj) => {
601            let dict = PyDict::new(py);
602            for (key, value) in obj.iter() {
603                dict.set_item(key, json_to_python(py, value)?)?;
604            }
605            Ok(dict.unbind().into())
606        }
607
608        JValue::Lambda { .. } | JValue::Builtin { .. } | JValue::Regex { .. } => Ok(py.None()),
609
610        JValue::LazyPyDict(lazy) => Ok(lazy.py_object().clone_ref(py).into_any()),
611    }
612}
613
614/// Build an `EvaluatorOptions` from the Python-facing `timeout`/`max_stack_depth`/
615/// `max_sequence_length` kwargs used by `compile()` and the free-standing `evaluate()`.
616#[cfg(feature = "python")]
617fn build_evaluator_options(
618    timeout: Option<u64>,
619    max_stack_depth: Option<usize>,
620    max_sequence_length: Option<usize>,
621) -> evaluator::EvaluatorOptions {
622    evaluator::EvaluatorOptions {
623        timeout_ms: timeout,
624        max_stack_depth,
625        max_sequence_length,
626    }
627}
628
629/// Create an evaluator, optionally configured with Python bindings
630#[cfg(feature = "python")]
631fn create_evaluator(
632    py: Python,
633    bindings: Option<Py<PyAny>>,
634    options: evaluator::EvaluatorOptions,
635) -> PyResult<evaluator::Evaluator> {
636    let mut context = evaluator::Context::new();
637    if let Some(bindings_obj) = bindings {
638        let bindings_json = python_to_json(py, &bindings_obj)?;
639        if let JValue::Object(map) = bindings_json {
640            for (key, value) in map.iter() {
641                context.bind(key.clone(), value.clone());
642            }
643        } else {
644            return Err(PyTypeError::new_err("bindings must be a dictionary"));
645        }
646    }
647    Ok(evaluator::Evaluator::with_options(context, options))
648}
649
650/// Convert an EvaluatorError to a PyErr
651#[cfg(feature = "python")]
652fn evaluator_error_to_py(e: evaluator::EvaluatorError) -> PyErr {
653    match e {
654        evaluator::EvaluatorError::PyConversionError(m) => PyTypeError::new_err(m),
655        other => PyValueError::new_err(other.message().to_string()),
656    }
657}
658
659/// Convert a ParserError to a PyErr
660#[cfg(feature = "python")]
661fn parser_error_to_py(e: parser::ParserError) -> PyErr {
662    PyValueError::new_err(e.display_message())
663}
664
665/// JSONata Python module
666#[cfg(feature = "python")]
667#[pymodule]
668fn _jsonatapy(m: &Bound<'_, PyModule>) -> PyResult<()> {
669    // Seed the tree-walker toggle from the environment once, at import time.
670    FORCE_TREE_WALKER.store(
671        std::env::var_os("JSONATAPY_FORCE_TREE_WALKER").is_some_and(|v| !v.is_empty() && v != "0"),
672        std::sync::atomic::Ordering::Relaxed,
673    );
674    m.add_function(wrap_pyfunction!(compile, m)?)?;
675    m.add_function(wrap_pyfunction!(evaluate, m)?)?;
676    m.add_function(wrap_pyfunction!(_set_force_tree_walker, m)?)?;
677    m.add_function(wrap_pyfunction!(_get_force_tree_walker, m)?)?;
678    m.add_class::<JsonataExpression>()?;
679    m.add_class::<JsonataData>()?;
680
681    // Add version info
682    m.add("__version__", env!("CARGO_PKG_VERSION"))?;
683    m.add("__jsonata_version__", JSONATA_REFERENCE_VERSION)?;
684
685    Ok(())
686}
687
688#[cfg(test)]
689mod tests {
690    #[test]
691    fn test_module_creation() {
692        // Basic smoke test
693        assert!(!env!("CARGO_PKG_VERSION").is_empty());
694    }
695
696    mod parser_error_handling {
697        use super::super::*;
698
699        // These test ParserError::display_message() directly (a plain string
700        // method) rather than parser_error_to_py(), which constructs a
701        // PyErr -- that requires an initialized Python interpreter, which
702        // isn't available under a bare `cargo test --all-features` run (no
703        // embedded interpreter, unlike the maturin-built extension loaded
704        // into a live Python process). The formatting logic under test is
705        // identical either way.
706
707        #[test]
708        fn test_parser_error_to_py_coded_error_no_prefix() {
709            // Test that coded errors (like S0214) are passed through without "Parse error: " prefix
710            let coded_error = parser::ParserError::Coded {
711                code: "S0214",
712                message: "Expected a variable reference after @".to_string(),
713            };
714            let msg = coded_error.display_message();
715
716            // The message should start with the code, not "Parse error: "
717            assert!(
718                msg.starts_with("S0214:"),
719                "Expected message to start with 'S0214:', got: {}",
720                msg
721            );
722            assert!(
723                !msg.starts_with("Parse error:"),
724                "Expected no 'Parse error:' prefix, got: {}",
725                msg
726            );
727        }
728
729        #[test]
730        fn test_parser_error_to_py_non_coded_error_with_prefix() {
731            // Test that non-coded errors still get the "Parse error: " prefix
732            let non_coded_error = parser::ParserError::UnexpectedToken("foo".to_string());
733            let msg = non_coded_error.display_message();
734
735            // The message should have the "Parse error: " prefix
736            assert!(
737                msg.starts_with("Parse error:"),
738                "Expected message to start with 'Parse error:', got: {}",
739                msg
740            );
741        }
742    }
743}