Skip to main content

xacro_rs/eval/interpreter/
init.rs

1use super::constants::BUILTIN_CONSTANTS;
2use pyisheval::{Interpreter, Value};
3
4/// Initialize an Interpreter with builtin constants and math functions
5///
6/// This ensures all interpreters have access to:
7/// - Math constants: pi, e, tau, M_PI
8/// - Math functions: radians(), degrees()
9///
10/// Note: inf and nan are NOT initialized here - they are injected directly into
11/// the context HashMap in build_pyisheval_context() to bypass parsing issues.
12///
13/// Note: Native math functions (cos, sin, tan, etc.) are handled via preprocessing
14/// in evaluate_expression() rather than as lambda functions, because pyisheval
15/// cannot call native Rust functions.
16///
17/// # Returns
18/// A fully initialized Interpreter ready for expression evaluation
19pub(crate) fn init_interpreter() -> Interpreter {
20    let mut interp = Interpreter::new();
21
22    // Initialize math constants in the interpreter
23    // These are loaded directly into the interpreter's environment for use in expressions
24    // Note: inf and nan are NOT in BUILTIN_CONSTANTS (pyisheval can't parse them as literals)
25    // They are injected directly into the context map in build_pyisheval_context()
26    for (name, value) in BUILTIN_CONSTANTS {
27        if let Err(e) = interp.eval(&format!("{} = {}", name, value)) {
28            log::warn!(
29                "Could not initialize built-in constant '{}': {}. \
30                 This constant will not be available in expressions.",
31                name,
32                e
33            );
34        }
35    }
36
37    // Add math conversion functions as lambda expressions directly in the interpreter
38    // This makes them available as callable functions in all expressions
39    if let Err(e) = interp.eval("radians = lambda x: x * pi / 180") {
40        log::warn!(
41            "Could not define built-in function 'radians': {}. \
42             This function will not be available in expressions. \
43             (May be due to missing 'pi' constant)",
44            e
45        );
46    }
47    if let Err(e) = interp.eval("degrees = lambda x: x * 180 / pi") {
48        log::warn!(
49            "Could not define built-in function 'degrees': {}. \
50             This function will not be available in expressions. \
51             (May be due to missing 'pi' constant)",
52            e
53        );
54    }
55
56    interp
57}
58
59#[derive(Debug, thiserror::Error)]
60pub enum EvalError {
61    #[error("Failed to evaluate expression '{expr}': {source}")]
62    PyishEval {
63        expr: String,
64        #[source]
65        source: pyisheval::EvalError,
66    },
67
68    #[error("Xacro conditional \"{condition}\" evaluated to \"{evaluated}\", which is not a boolean expression.")]
69    InvalidBoolean {
70        condition: String,
71        evaluated: String,
72    },
73}
74
75/// Format a pyisheval Value to match Python xacro's output format
76///
77/// Python xacro uses Python's int/float distinction for formatting:
78/// - Integer arithmetic (2+3) produces int → formats as "5" (no decimal)
79/// - Float arithmetic (2.5*2) produces float → formats as "5.0" (with decimal)
80///
81/// Since pyisheval only has f64 (no int type), we approximate this by checking
82/// if the f64 value is mathematically a whole number using fract() == 0.0.
83///
84/// # Arguments
85/// * `value` - The pyisheval Value to format
86/// * `force_float` - Whether to keep .0 for whole numbers (true for float context)
87pub(crate) fn format_value_python_style(
88    value: &Value,
89    force_float: bool,
90) -> String {
91    match value {
92        Value::Number(n) if n.is_finite() => {
93            // Python's str() for floats switches to scientific notation at 1e16
94            const PYTHON_SCIENTIFIC_THRESHOLD: f64 = 1e16;
95
96            if n.fract() == 0.0 && n.abs() < PYTHON_SCIENTIFIC_THRESHOLD {
97                // Whole number
98                if force_float {
99                    // Float context: keep .0 for whole numbers
100                    format!("{:.1}", n) // "1.0" not "1"
101                } else {
102                    // Int context: strip .0
103                    format!("{:.0}", n) // "1" not "1.0"
104                }
105            } else {
106                // Has fractional part or is a large number: use default formatting
107                n.to_string()
108            }
109        }
110        _ => value.to_string(),
111    }
112}