Skip to main content

brainwires_code_interpreters/languages/
rhai.rs

1//! Rhai executor - Native Rust scripting language
2//!
3//! Rhai is the fastest option as it's a pure Rust scripting engine.
4//! It has excellent integration with Rust types and functions.
5//!
6//! ## Features
7//! - Native Rust execution (no interpreter overhead)
8//! - Built-in safety limits (max operations, memory)
9//! - Good for configuration scripts and simple logic
10//!
11//! ## Limitations
12//! - Smaller standard library than Python/JS
13//! - Less familiar syntax for most users
14
15use rhai::{Dynamic, Engine, EvalAltResult, Scope};
16use std::sync::{Arc, Mutex};
17use std::time::Instant;
18
19use super::{LanguageExecutor, get_limits, truncate_output};
20use crate::types::{ExecutionLimits, ExecutionRequest, ExecutionResult};
21
22/// Rhai code executor
23pub struct RhaiExecutor {
24    _limits: ExecutionLimits,
25}
26
27impl RhaiExecutor {
28    /// Create a new Rhai executor with default limits
29    pub fn new() -> Self {
30        Self {
31            _limits: ExecutionLimits::default(),
32        }
33    }
34
35    /// Create a new Rhai executor with custom limits
36    pub fn with_limits(limits: ExecutionLimits) -> Self {
37        Self { _limits: limits }
38    }
39
40    /// Configure a Rhai engine with safety limits
41    fn configure_engine(&self, limits: &ExecutionLimits) -> Engine {
42        let mut engine = Engine::new();
43
44        // Apply safety limits
45        engine.set_max_operations(limits.max_operations);
46        engine.set_max_string_size(limits.max_string_length);
47        engine.set_max_array_size(limits.max_array_length);
48        engine.set_max_map_size(limits.max_map_size);
49        engine.set_max_expr_depths(
50            limits.max_call_depth as usize,
51            limits.max_call_depth as usize,
52        );
53
54        // Disable potentially dangerous features for sandboxing
55        engine.set_allow_looping(true); // Allow loops (controlled by max_operations)
56        engine.set_strict_variables(true); // Require variable declaration
57
58        engine
59    }
60
61    /// Inject context variables into the scope
62    fn inject_context(&self, scope: &mut Scope, context: &serde_json::Value) {
63        if let serde_json::Value::Object(map) = context {
64            for (key, value) in map {
65                let dynamic_value = json_to_dynamic(value);
66                scope.push(key.clone(), dynamic_value);
67            }
68        }
69    }
70
71    /// Execute Rhai code
72    pub fn execute_code(&self, request: &ExecutionRequest) -> ExecutionResult {
73        let limits = get_limits(request);
74        let engine = self.configure_engine(&limits);
75
76        let start = Instant::now();
77
78        // Create scope and inject context
79        let mut scope = Scope::new();
80        if let Some(context) = &request.context {
81            self.inject_context(&mut scope, context);
82        }
83
84        // Capture print output
85        let output = Arc::new(Mutex::new(Vec::<String>::new()));
86        let output_clone = output.clone();
87
88        // Capture print and debug output via engine callbacks
89        let mut engine = engine;
90        engine.on_print(move |s| {
91            if let Ok(mut out) = output_clone.lock() {
92                out.push(s.to_string());
93            }
94        });
95
96        let output_clone2 = output.clone();
97        engine.on_debug(move |s, _src, _pos| {
98            if let Ok(mut out) = output_clone2.lock() {
99                out.push(format!("[DEBUG] {}", s));
100            }
101        });
102
103        // Execute the script
104        let result: Result<Dynamic, Box<EvalAltResult>> =
105            engine.eval_with_scope(&mut scope, &request.code);
106        let timing_ms = start.elapsed().as_millis() as u64;
107
108        // Get captured output
109        let stdout = output.lock().map(|out| out.join("\n")).unwrap_or_default();
110        let stdout = truncate_output(&stdout, limits.max_output_bytes);
111
112        match result {
113            Ok(value) => {
114                let result_value = dynamic_to_json(&value);
115                let mut stdout_with_result = stdout;
116
117                // If there's a non-unit result, append it to stdout
118                if !value.is_unit() {
119                    if !stdout_with_result.is_empty() {
120                        stdout_with_result.push('\n');
121                    }
122                    stdout_with_result.push_str(&format!("{}", value));
123                }
124
125                ExecutionResult {
126                    success: true,
127                    stdout: stdout_with_result,
128                    stderr: String::new(),
129                    result: result_value,
130                    error: None,
131                    timing_ms,
132                    memory_used_bytes: None,
133                    operations_count: None,
134                }
135            }
136            Err(e) => {
137                let error_message = format_rhai_error(&e);
138                ExecutionResult {
139                    success: false,
140                    stdout,
141                    stderr: error_message.clone(),
142                    result: None,
143                    error: Some(error_message),
144                    timing_ms,
145                    memory_used_bytes: None,
146                    operations_count: None,
147                }
148            }
149        }
150    }
151}
152
153impl Default for RhaiExecutor {
154    fn default() -> Self {
155        Self::new()
156    }
157}
158
159impl LanguageExecutor for RhaiExecutor {
160    fn execute(&self, request: &ExecutionRequest) -> ExecutionResult {
161        self.execute_code(request)
162    }
163
164    fn language_name(&self) -> &'static str {
165        "rhai"
166    }
167
168    fn language_version(&self) -> String {
169        // Rhai version is determined at compile time
170        "1.20".to_string()
171    }
172}
173
174/// Convert JSON value to Rhai Dynamic
175fn json_to_dynamic(value: &serde_json::Value) -> Dynamic {
176    match value {
177        serde_json::Value::Null => Dynamic::UNIT,
178        serde_json::Value::Bool(b) => Dynamic::from(*b),
179        serde_json::Value::Number(n) => {
180            if let Some(i) = n.as_i64() {
181                Dynamic::from(i)
182            } else if let Some(f) = n.as_f64() {
183                Dynamic::from(f)
184            } else {
185                Dynamic::UNIT
186            }
187        }
188        serde_json::Value::String(s) => Dynamic::from(s.clone()),
189        serde_json::Value::Array(arr) => {
190            let vec: Vec<Dynamic> = arr.iter().map(json_to_dynamic).collect();
191            Dynamic::from(vec)
192        }
193        serde_json::Value::Object(obj) => {
194            let mut map = rhai::Map::new();
195            for (k, v) in obj {
196                map.insert(k.clone().into(), json_to_dynamic(v));
197            }
198            Dynamic::from(map)
199        }
200    }
201}
202
203/// Convert Rhai Dynamic to JSON value
204fn dynamic_to_json(value: &Dynamic) -> Option<serde_json::Value> {
205    if value.is_unit() {
206        return None;
207    }
208
209    if value.is_bool() {
210        return Some(serde_json::Value::Bool(value.as_bool().unwrap_or(false)));
211    }
212
213    if value.is_int() {
214        return Some(serde_json::Value::Number(serde_json::Number::from(
215            value.as_int().unwrap_or(0),
216        )));
217    }
218
219    if value.is_float() {
220        if let Ok(f) = value.as_float()
221            && let Some(n) = serde_json::Number::from_f64(f)
222        {
223            return Some(serde_json::Value::Number(n));
224        }
225        return None;
226    }
227
228    if value.is_string() {
229        return Some(serde_json::Value::String(
230            value.clone().into_string().unwrap_or_default(),
231        ));
232    }
233
234    if value.is_array()
235        && let Ok(arr) = value.clone().into_array()
236    {
237        let json_arr: Vec<serde_json::Value> = arr
238            .into_iter()
239            .filter_map(|v| dynamic_to_json(&v))
240            .collect();
241        return Some(serde_json::Value::Array(json_arr));
242    }
243
244    if value.is_map()
245        && let Some(map) = value.clone().try_cast::<rhai::Map>()
246    {
247        let mut json_map = serde_json::Map::new();
248        for (k, v) in map {
249            if let Some(json_v) = dynamic_to_json(&v) {
250                json_map.insert(k.to_string(), json_v);
251            }
252        }
253        return Some(serde_json::Value::Object(json_map));
254    }
255
256    // Default: convert to string representation
257    Some(serde_json::Value::String(format!("{}", value)))
258}
259
260/// Format Rhai error for user display
261fn format_rhai_error(error: &EvalAltResult) -> String {
262    match error {
263        EvalAltResult::ErrorTooManyOperations(_) => {
264            "Operation limit exceeded - possible infinite loop".to_string()
265        }
266        EvalAltResult::ErrorDataTooLarge(msg, _) => {
267            format!("Data too large: {}", msg)
268        }
269        EvalAltResult::ErrorStackOverflow(_) => {
270            "Stack overflow - too many nested calls".to_string()
271        }
272        EvalAltResult::ErrorParsing(parse_error, _) => {
273            format!("Syntax error: {}", parse_error)
274        }
275        EvalAltResult::ErrorVariableNotFound(name, _) => {
276            format!("Variable not found: {}", name)
277        }
278        EvalAltResult::ErrorFunctionNotFound(name, _) => {
279            format!("Function not found: {}", name)
280        }
281        _ => format!("{}", error),
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use crate::types::Language;
289
290    fn make_request(code: &str) -> ExecutionRequest {
291        ExecutionRequest {
292            language: Language::Rhai,
293            code: code.to_string(),
294            ..Default::default()
295        }
296    }
297
298    #[test]
299    fn test_simple_expression() {
300        let executor = RhaiExecutor::new();
301        let result = executor.execute(&make_request("1 + 2"));
302        assert!(result.success);
303        assert!(result.stdout.contains("3"));
304    }
305
306    #[test]
307    fn test_string_expression() {
308        let executor = RhaiExecutor::new();
309        let result = executor.execute(&make_request(r#""Hello, World!""#));
310        assert!(result.success);
311        assert!(result.stdout.contains("Hello, World!"));
312    }
313
314    #[test]
315    fn test_variable_declaration() {
316        let executor = RhaiExecutor::new();
317        let result = executor.execute(&make_request(
318            r#"
319            let x = 10;
320            let y = 20;
321            x + y
322            "#,
323        ));
324        assert!(result.success);
325        assert!(result.stdout.contains("30"));
326    }
327
328    #[test]
329    fn test_loop() {
330        let executor = RhaiExecutor::new();
331        let result = executor.execute(&make_request(
332            r#"
333            let sum = 0;
334            for i in 0..10 {
335                sum += i;
336            }
337            sum
338            "#,
339        ));
340        assert!(result.success);
341        assert!(result.stdout.contains("45")); // Sum of 0..9
342    }
343
344    #[test]
345    fn test_syntax_error() {
346        let executor = RhaiExecutor::new();
347        let result = executor.execute(&make_request("let x = "));
348        assert!(!result.success);
349        assert!(result.error.is_some());
350    }
351
352    #[test]
353    fn test_undefined_variable() {
354        let executor = RhaiExecutor::new();
355        let result = executor.execute(&make_request("undefined_var"));
356        assert!(!result.success);
357        let err = result.error.unwrap();
358        assert!(
359            err.contains("not found") || err.contains("Undefined"),
360            "Unexpected error: {}",
361            err
362        );
363    }
364
365    #[test]
366    fn test_context_injection() {
367        let executor = RhaiExecutor::new();
368        let mut request = make_request("x + y");
369        request.context = Some(serde_json::json!({
370            "x": 10,
371            "y": 20
372        }));
373        let result = executor.execute(&request);
374        assert!(result.success);
375        assert!(result.stdout.contains("30"));
376    }
377
378    #[test]
379    fn test_array_operations() {
380        let executor = RhaiExecutor::new();
381        let result = executor.execute(&make_request(
382            r#"
383            let arr = [1, 2, 3, 4, 5];
384            arr.len()
385            "#,
386        ));
387        assert!(result.success);
388        assert!(result.stdout.contains("5"));
389    }
390
391    #[test]
392    fn test_map_operations() {
393        let executor = RhaiExecutor::new();
394        let result = executor.execute(&make_request(
395            r#"
396            let map = #{
397                name: "test",
398                value: 42
399            };
400            map.value
401            "#,
402        ));
403        assert!(result.success);
404        assert!(result.stdout.contains("42"));
405    }
406}