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        // Register print function
89        let mut engine = engine;
90        engine.register_fn("print", move |s: &str| {
91            if let Ok(mut out) = output_clone.lock() {
92                out.push(s.to_string());
93            }
94        });
95
96        // Also capture debug output
97        let output_clone2 = output.clone();
98        engine.register_fn("debug", move |s: Dynamic| {
99            if let Ok(mut out) = output_clone2.lock() {
100                out.push(format!("[DEBUG] {:?}", s));
101            }
102        });
103
104        // Execute the script
105        let result: Result<Dynamic, Box<EvalAltResult>> =
106            engine.eval_with_scope(&mut scope, &request.code);
107        let timing_ms = start.elapsed().as_millis() as u64;
108
109        // Get captured output
110        let stdout = output.lock().map(|out| out.join("\n")).unwrap_or_default();
111        let stdout = truncate_output(&stdout, limits.max_output_bytes);
112
113        match result {
114            Ok(value) => {
115                let result_value = dynamic_to_json(&value);
116                let mut stdout_with_result = stdout;
117
118                // If there's a non-unit result, append it to stdout
119                if !value.is_unit() {
120                    if !stdout_with_result.is_empty() {
121                        stdout_with_result.push('\n');
122                    }
123                    stdout_with_result.push_str(&format!("{}", value));
124                }
125
126                ExecutionResult {
127                    success: true,
128                    stdout: stdout_with_result,
129                    stderr: String::new(),
130                    result: result_value,
131                    error: None,
132                    timing_ms,
133                    memory_used_bytes: None,
134                    operations_count: None,
135                }
136            }
137            Err(e) => {
138                let error_message = format_rhai_error(&e);
139                ExecutionResult {
140                    success: false,
141                    stdout,
142                    stderr: error_message.clone(),
143                    result: None,
144                    error: Some(error_message),
145                    timing_ms,
146                    memory_used_bytes: None,
147                    operations_count: None,
148                }
149            }
150        }
151    }
152}
153
154impl Default for RhaiExecutor {
155    fn default() -> Self {
156        Self::new()
157    }
158}
159
160impl LanguageExecutor for RhaiExecutor {
161    fn execute(&self, request: &ExecutionRequest) -> ExecutionResult {
162        self.execute_code(request)
163    }
164
165    fn language_name(&self) -> &'static str {
166        "rhai"
167    }
168
169    fn language_version(&self) -> String {
170        // Rhai version is determined at compile time
171        "1.20".to_string()
172    }
173}
174
175/// Convert JSON value to Rhai Dynamic
176fn json_to_dynamic(value: &serde_json::Value) -> Dynamic {
177    match value {
178        serde_json::Value::Null => Dynamic::UNIT,
179        serde_json::Value::Bool(b) => Dynamic::from(*b),
180        serde_json::Value::Number(n) => {
181            if let Some(i) = n.as_i64() {
182                Dynamic::from(i)
183            } else if let Some(f) = n.as_f64() {
184                Dynamic::from(f)
185            } else {
186                Dynamic::UNIT
187            }
188        }
189        serde_json::Value::String(s) => Dynamic::from(s.clone()),
190        serde_json::Value::Array(arr) => {
191            let vec: Vec<Dynamic> = arr.iter().map(json_to_dynamic).collect();
192            Dynamic::from(vec)
193        }
194        serde_json::Value::Object(obj) => {
195            let mut map = rhai::Map::new();
196            for (k, v) in obj {
197                map.insert(k.clone().into(), json_to_dynamic(v));
198            }
199            Dynamic::from(map)
200        }
201    }
202}
203
204/// Convert Rhai Dynamic to JSON value
205fn dynamic_to_json(value: &Dynamic) -> Option<serde_json::Value> {
206    if value.is_unit() {
207        return None;
208    }
209
210    if value.is_bool() {
211        return Some(serde_json::Value::Bool(value.as_bool().unwrap_or(false)));
212    }
213
214    if value.is_int() {
215        return Some(serde_json::Value::Number(serde_json::Number::from(
216            value.as_int().unwrap_or(0),
217        )));
218    }
219
220    if value.is_float() {
221        if let Ok(f) = value.as_float()
222            && let Some(n) = serde_json::Number::from_f64(f)
223        {
224            return Some(serde_json::Value::Number(n));
225        }
226        return None;
227    }
228
229    if value.is_string() {
230        return Some(serde_json::Value::String(
231            value.clone().into_string().unwrap_or_default(),
232        ));
233    }
234
235    if value.is_array()
236        && let Ok(arr) = value.clone().into_array()
237    {
238        let json_arr: Vec<serde_json::Value> = arr
239            .into_iter()
240            .filter_map(|v| dynamic_to_json(&v))
241            .collect();
242        return Some(serde_json::Value::Array(json_arr));
243    }
244
245    if value.is_map()
246        && let Some(map) = value.clone().try_cast::<rhai::Map>()
247    {
248        let mut json_map = serde_json::Map::new();
249        for (k, v) in map {
250            if let Some(json_v) = dynamic_to_json(&v) {
251                json_map.insert(k.to_string(), json_v);
252            }
253        }
254        return Some(serde_json::Value::Object(json_map));
255    }
256
257    // Default: convert to string representation
258    Some(serde_json::Value::String(format!("{}", value)))
259}
260
261/// Format Rhai error for user display
262fn format_rhai_error(error: &EvalAltResult) -> String {
263    match error {
264        EvalAltResult::ErrorTooManyOperations(_) => {
265            "Operation limit exceeded - possible infinite loop".to_string()
266        }
267        EvalAltResult::ErrorDataTooLarge(msg, _) => {
268            format!("Data too large: {}", msg)
269        }
270        EvalAltResult::ErrorStackOverflow(_) => {
271            "Stack overflow - too many nested calls".to_string()
272        }
273        EvalAltResult::ErrorParsing(parse_error, _) => {
274            format!("Syntax error: {}", parse_error)
275        }
276        EvalAltResult::ErrorVariableNotFound(name, _) => {
277            format!("Variable not found: {}", name)
278        }
279        EvalAltResult::ErrorFunctionNotFound(name, _) => {
280            format!("Function not found: {}", name)
281        }
282        _ => format!("{}", error),
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use crate::types::Language;
290
291    fn make_request(code: &str) -> ExecutionRequest {
292        ExecutionRequest {
293            language: Language::Rhai,
294            code: code.to_string(),
295            ..Default::default()
296        }
297    }
298
299    #[test]
300    fn test_simple_expression() {
301        let executor = RhaiExecutor::new();
302        let result = executor.execute(&make_request("1 + 2"));
303        assert!(result.success);
304        assert!(result.stdout.contains("3"));
305    }
306
307    #[test]
308    fn test_string_expression() {
309        let executor = RhaiExecutor::new();
310        let result = executor.execute(&make_request(r#""Hello, World!""#));
311        assert!(result.success);
312        assert!(result.stdout.contains("Hello, World!"));
313    }
314
315    #[test]
316    fn test_variable_declaration() {
317        let executor = RhaiExecutor::new();
318        let result = executor.execute(&make_request(
319            r#"
320            let x = 10;
321            let y = 20;
322            x + y
323            "#,
324        ));
325        assert!(result.success);
326        assert!(result.stdout.contains("30"));
327    }
328
329    #[test]
330    fn test_loop() {
331        let executor = RhaiExecutor::new();
332        let result = executor.execute(&make_request(
333            r#"
334            let sum = 0;
335            for i in 0..10 {
336                sum += i;
337            }
338            sum
339            "#,
340        ));
341        assert!(result.success);
342        assert!(result.stdout.contains("45")); // Sum of 0..9
343    }
344
345    #[test]
346    fn test_syntax_error() {
347        let executor = RhaiExecutor::new();
348        let result = executor.execute(&make_request("let x = "));
349        assert!(!result.success);
350        assert!(result.error.is_some());
351    }
352
353    #[test]
354    fn test_undefined_variable() {
355        let executor = RhaiExecutor::new();
356        let result = executor.execute(&make_request("undefined_var"));
357        assert!(!result.success);
358        let err = result.error.unwrap();
359        assert!(
360            err.contains("not found") || err.contains("Undefined"),
361            "Unexpected error: {}",
362            err
363        );
364    }
365
366    #[test]
367    fn test_context_injection() {
368        let executor = RhaiExecutor::new();
369        let mut request = make_request("x + y");
370        request.context = Some(serde_json::json!({
371            "x": 10,
372            "y": 20
373        }));
374        let result = executor.execute(&request);
375        assert!(result.success);
376        assert!(result.stdout.contains("30"));
377    }
378
379    #[test]
380    fn test_array_operations() {
381        let executor = RhaiExecutor::new();
382        let result = executor.execute(&make_request(
383            r#"
384            let arr = [1, 2, 3, 4, 5];
385            arr.len()
386            "#,
387        ));
388        assert!(result.success);
389        assert!(result.stdout.contains("5"));
390    }
391
392    #[test]
393    fn test_map_operations() {
394        let executor = RhaiExecutor::new();
395        let result = executor.execute(&make_request(
396            r#"
397            let map = #{
398                name: "test",
399                value: 42
400            };
401            map.value
402            "#,
403        ));
404        assert!(result.success);
405        assert!(result.stdout.contains("42"));
406    }
407}