brainwires_code_interpreters/languages/
rhai.rs1use 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
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(limits.max_call_depth as usize, limits.max_call_depth as usize);
50
51 engine.set_allow_looping(true); engine.set_strict_variables(true); engine
56 }
57
58 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 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 let mut scope = Scope::new();
77 if let Some(context) = &request.context {
78 self.inject_context(&mut scope, context);
79 }
80
81 let output = Arc::new(Mutex::new(Vec::<String>::new()));
83 let output_clone = output.clone();
84
85 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 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 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 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 !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 "1.20".to_string()
171 }
172}
173
174fn 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
203fn 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 Some(serde_json::Value::String(format!("{}", value)))
255}
256
257fn 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")); }
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}