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}