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::{get_limits, truncate_output, LanguageExecutor};
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(limits.max_call_depth as usize, limits.max_call_depth as usize);
50
51        // Disable potentially dangerous features for sandboxing
52        engine.set_allow_looping(true); // Allow loops (controlled by max_operations)
53        engine.set_strict_variables(true); // Require variable declaration
54
55        engine
56    }
57
58    /// Inject context variables into the scope
59    fn inject_context(&self, scope: &mut Scope, context: &serde_json::Value) {
60        if let serde_json::Value::Object(map) = context {
61            for (key, value) in map {
62                let dynamic_value = json_to_dynamic(value);
63                scope.push(key.clone(), dynamic_value);
64            }
65        }
66    }
67
68    /// Execute Rhai code
69    pub fn execute_code(&self, request: &ExecutionRequest) -> ExecutionResult {
70        let limits = get_limits(request);
71        let engine = self.configure_engine(&limits);
72
73        let start = Instant::now();
74
75        // Create scope and inject context
76        let mut scope = Scope::new();
77        if let Some(context) = &request.context {
78            self.inject_context(&mut scope, context);
79        }
80
81        // Capture print output
82        let output = Arc::new(Mutex::new(Vec::<String>::new()));
83        let output_clone = output.clone();
84
85        // Register print function
86        let mut engine = engine;
87        engine.register_fn("print", move |s: &str| {
88            if let Ok(mut out) = output_clone.lock() {
89                out.push(s.to_string());
90            }
91        });
92
93        // Also capture debug output
94        let output_clone2 = output.clone();
95        engine.register_fn("debug", move |s: Dynamic| {
96            if let Ok(mut out) = output_clone2.lock() {
97                out.push(format!("[DEBUG] {:?}", s));
98            }
99        });
100
101        // Execute the script
102        let result: Result<Dynamic, Box<EvalAltResult>> = engine.eval_with_scope(&mut scope, &request.code);
103        let timing_ms = start.elapsed().as_millis() as u64;
104
105        // Get captured output
106        let stdout = output
107            .lock()
108            .map(|out| out.join("\n"))
109            .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(
215            serde_json::Number::from(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                return Some(serde_json::Value::Number(n));
223            }
224        return None;
225    }
226
227    if value.is_string() {
228        return Some(serde_json::Value::String(
229            value.clone().into_string().unwrap_or_default(),
230        ));
231    }
232
233    if value.is_array()
234        && let Ok(arr) = value.clone().into_array() {
235            let json_arr: Vec<serde_json::Value> = arr
236                .into_iter()
237                .filter_map(|v| dynamic_to_json(&v))
238                .collect();
239            return Some(serde_json::Value::Array(json_arr));
240        }
241
242    if value.is_map()
243        && let Some(map) = value.clone().try_cast::<rhai::Map>() {
244            let mut json_map = serde_json::Map::new();
245            for (k, v) in map {
246                if let Some(json_v) = dynamic_to_json(&v) {
247                    json_map.insert(k.to_string(), json_v);
248                }
249            }
250            return Some(serde_json::Value::Object(json_map));
251        }
252
253    // Default: convert to string representation
254    Some(serde_json::Value::String(format!("{}", value)))
255}
256
257/// Format Rhai error for user display
258fn format_rhai_error(error: &EvalAltResult) -> String {
259    match error {
260        EvalAltResult::ErrorTooManyOperations(_) => {
261            "Operation limit exceeded - possible infinite loop".to_string()
262        }
263        EvalAltResult::ErrorDataTooLarge(msg, _) => {
264            format!("Data too large: {}", msg)
265        }
266        EvalAltResult::ErrorStackOverflow(_) => {
267            "Stack overflow - too many nested calls".to_string()
268        }
269        EvalAltResult::ErrorParsing(parse_error, _) => {
270            format!("Syntax error: {}", parse_error)
271        }
272        EvalAltResult::ErrorVariableNotFound(name, _) => {
273            format!("Variable not found: {}", name)
274        }
275        EvalAltResult::ErrorFunctionNotFound(name, _) => {
276            format!("Function not found: {}", name)
277        }
278        _ => format!("{}", error),
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285    use crate::types::Language;
286
287    fn make_request(code: &str) -> ExecutionRequest {
288        ExecutionRequest {
289            language: Language::Rhai,
290            code: code.to_string(),
291            ..Default::default()
292        }
293    }
294
295    #[test]
296    fn test_simple_expression() {
297        let executor = RhaiExecutor::new();
298        let result = executor.execute(&make_request("1 + 2"));
299        assert!(result.success);
300        assert!(result.stdout.contains("3"));
301    }
302
303    #[test]
304    fn test_string_expression() {
305        let executor = RhaiExecutor::new();
306        let result = executor.execute(&make_request(r#""Hello, World!""#));
307        assert!(result.success);
308        assert!(result.stdout.contains("Hello, World!"));
309    }
310
311    #[test]
312    fn test_variable_declaration() {
313        let executor = RhaiExecutor::new();
314        let result = executor.execute(&make_request(
315            r#"
316            let x = 10;
317            let y = 20;
318            x + y
319            "#,
320        ));
321        assert!(result.success);
322        assert!(result.stdout.contains("30"));
323    }
324
325    #[test]
326    fn test_loop() {
327        let executor = RhaiExecutor::new();
328        let result = executor.execute(&make_request(
329            r#"
330            let sum = 0;
331            for i in 0..10 {
332                sum += i;
333            }
334            sum
335            "#,
336        ));
337        assert!(result.success);
338        assert!(result.stdout.contains("45")); // Sum of 0..9
339    }
340
341    #[test]
342    fn test_syntax_error() {
343        let executor = RhaiExecutor::new();
344        let result = executor.execute(&make_request("let x = "));
345        assert!(!result.success);
346        assert!(result.error.is_some());
347    }
348
349    #[test]
350    fn test_undefined_variable() {
351        let executor = RhaiExecutor::new();
352        let result = executor.execute(&make_request("undefined_var"));
353        assert!(!result.success);
354        let err = result.error.unwrap();
355        assert!(
356            err.contains("not found") || err.contains("Undefined"),
357            "Unexpected error: {}",
358            err
359        );
360    }
361
362    #[test]
363    fn test_context_injection() {
364        let executor = RhaiExecutor::new();
365        let mut request = make_request("x + y");
366        request.context = Some(serde_json::json!({
367            "x": 10,
368            "y": 20
369        }));
370        let result = executor.execute(&request);
371        assert!(result.success);
372        assert!(result.stdout.contains("30"));
373    }
374
375    #[test]
376    fn test_array_operations() {
377        let executor = RhaiExecutor::new();
378        let result = executor.execute(&make_request(
379            r#"
380            let arr = [1, 2, 3, 4, 5];
381            arr.len()
382            "#,
383        ));
384        assert!(result.success);
385        assert!(result.stdout.contains("5"));
386    }
387
388    #[test]
389    fn test_map_operations() {
390        let executor = RhaiExecutor::new();
391        let result = executor.execute(&make_request(
392            r#"
393            let map = #{
394                name: "test",
395                value: 42
396            };
397            map.value
398            "#,
399        ));
400        assert!(result.success);
401        assert!(result.stdout.contains("42"));
402    }
403}