brainwires_code_interpreters/languages/
rhai.rs1use 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
22pub struct RhaiExecutor {
24 _limits: ExecutionLimits,
25}
26
27impl RhaiExecutor {
28 pub fn new() -> Self {
30 Self {
31 _limits: ExecutionLimits::default(),
32 }
33 }
34
35 pub fn with_limits(limits: ExecutionLimits) -> Self {
37 Self { _limits: limits }
38 }
39
40 fn configure_engine(&self, limits: &ExecutionLimits) -> Engine {
42 let mut engine = Engine::new();
43
44 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 engine.set_allow_looping(true); engine.set_strict_variables(true); engine
59 }
60
61 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 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 let mut scope = Scope::new();
80 if let Some(context) = &request.context {
81 self.inject_context(&mut scope, context);
82 }
83
84 let output = Arc::new(Mutex::new(Vec::<String>::new()));
86 let output_clone = output.clone();
87
88 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 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 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 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 !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 "1.20".to_string()
172 }
173}
174
175fn 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
204fn 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 Some(serde_json::Value::String(format!("{}", value)))
259}
260
261fn 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")); }
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}