1use anyhow::{Result, anyhow};
7use std::collections::HashMap;
8use std::time::{Duration, Instant};
9use std::fmt;
10
11use crate::runtime::repl::{Repl, Value};
12
13pub struct MagicRegistry {
19 commands: HashMap<String, Box<dyn MagicCommand>>,
20}
21
22impl MagicRegistry {
23 pub fn new() -> Self {
24 let mut registry = Self {
25 commands: HashMap::new(),
26 };
27
28 registry.register("time", Box::new(TimeMagic));
30 registry.register("timeit", Box::new(TimeitMagic::default()));
31 registry.register("run", Box::new(RunMagic));
32 registry.register("debug", Box::new(DebugMagic));
33 registry.register("profile", Box::new(ProfileMagic));
34 registry.register("whos", Box::new(WhosMagic));
35 registry.register("clear", Box::new(ClearMagic));
36 registry.register("reset", Box::new(ResetMagic));
37 registry.register("history", Box::new(HistoryMagic));
38 registry.register("save", Box::new(SaveMagic));
39 registry.register("load", Box::new(LoadMagic));
40 registry.register("pwd", Box::new(PwdMagic));
41 registry.register("cd", Box::new(CdMagic));
42 registry.register("ls", Box::new(LsMagic));
43
44 registry
45 }
46
47 pub fn register(&mut self, name: &str, command: Box<dyn MagicCommand>) {
49 self.commands.insert(name.to_string(), command);
50 }
51
52 pub fn is_magic(&self, input: &str) -> bool {
54 input.starts_with('%') || input.starts_with("%%")
55 }
56
57 pub fn execute(&mut self, repl: &mut Repl, input: &str) -> Result<MagicResult> {
59 if !self.is_magic(input) {
60 return Err(anyhow!("Not a magic command"));
61 }
62
63 let (is_cell_magic, command_line) = if input.starts_with("%%") {
65 (true, &input[2..])
66 } else {
67 (false, &input[1..])
68 };
69
70 let parts: Vec<&str> = command_line.split_whitespace().collect();
71 if parts.is_empty() {
72 return Err(anyhow!("Empty magic command"));
73 }
74
75 let command_name = parts[0];
76 let args = &parts[1..];
77
78 match self.commands.get(command_name) {
80 Some(command) => {
81 if is_cell_magic {
82 command.execute_cell(repl, args.join(" ").as_str())
83 } else {
84 command.execute_line(repl, args.join(" ").as_str())
85 }
86 }
87 None => Err(anyhow!("Unknown magic command: %{}", command_name)),
88 }
89 }
90
91 pub fn list_commands(&self) -> Vec<String> {
93 let mut commands: Vec<_> = self.commands.keys().cloned().collect();
94 commands.sort();
95 commands
96 }
97}
98
99impl Default for MagicRegistry {
100 fn default() -> Self {
101 Self::new()
102 }
103}
104
105#[derive(Debug, Clone)]
111pub enum MagicResult {
112 Text(String),
114 Timed { output: String, duration: Duration },
116 Profile(ProfileData),
118 Silent,
120}
121
122impl fmt::Display for MagicResult {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 match self {
125 MagicResult::Text(s) => write!(f, "{s}"),
126 MagicResult::Timed { output, duration } => {
127 write!(f, "{}\nExecution time: {:.3}s", output, duration.as_secs_f64())
128 }
129 MagicResult::Profile(data) => write!(f, "{data}"),
130 MagicResult::Silent => Ok(()),
131 }
132 }
133}
134
135pub trait MagicCommand: Send + Sync {
137 fn execute_line(&self, repl: &mut Repl, args: &str) -> Result<MagicResult>;
139
140 fn execute_cell(&self, repl: &mut Repl, args: &str) -> Result<MagicResult> {
142 self.execute_line(repl, args)
144 }
145
146 fn help(&self) -> &str;
148}
149
150struct TimeMagic;
156
157impl MagicCommand for TimeMagic {
158 fn execute_line(&self, repl: &mut Repl, args: &str) -> Result<MagicResult> {
159 if args.trim().is_empty() {
160 return Err(anyhow!("Usage: %time <expression>"));
161 }
162
163 let start = Instant::now();
164 let result = repl.eval(args)?;
165 let duration = start.elapsed();
166
167 Ok(MagicResult::Timed {
168 output: result,
169 duration,
170 })
171 }
172
173 fn help(&self) -> &'static str {
174 "Time execution of a single expression"
175 }
176}
177
178struct TimeitMagic {
180 default_runs: usize,
181}
182
183impl Default for TimeitMagic {
184 fn default() -> Self {
185 Self { default_runs: 1000 }
186 }
187}
188
189impl MagicCommand for TimeitMagic {
190 fn execute_line(&self, repl: &mut Repl, args: &str) -> Result<MagicResult> {
191 if args.trim().is_empty() {
192 return Err(anyhow!("Usage: %timeit [-n RUNS] <expression>"));
193 }
194
195 let (runs, expr) = if args.starts_with("-n ") {
197 let parts: Vec<&str> = args.splitn(3, ' ').collect();
198 if parts.len() < 3 {
199 return Err(anyhow!("Invalid -n syntax"));
200 }
201 let n = parts[1].parse::<usize>()
202 .map_err(|_| anyhow!("Invalid number of runs"))?;
203 (n, parts[2])
204 } else {
205 (self.default_runs, args)
206 };
207
208 repl.eval(expr)?;
210
211 let mut durations = Vec::with_capacity(runs);
213 for _ in 0..runs {
214 let start = Instant::now();
215 repl.eval(expr)?;
216 durations.push(start.elapsed());
217 }
218
219 let total: Duration = durations.iter().sum();
221 let mean = total / runs as u32;
222
223 durations.sort();
224 let min = durations[0];
225 let max = durations[runs - 1];
226 let median = if runs % 2 == 0 {
227 (durations[runs / 2 - 1] + durations[runs / 2]) / 2
228 } else {
229 durations[runs / 2]
230 };
231
232 let output = format!(
233 "{} loops, best of {}: {:.3}µs per loop\n\
234 min: {:.3}µs, median: {:.3}µs, max: {:.3}µs",
235 runs, runs,
236 mean.as_micros() as f64,
237 min.as_micros() as f64,
238 median.as_micros() as f64,
239 max.as_micros() as f64
240 );
241
242 Ok(MagicResult::Text(output))
243 }
244
245 fn help(&self) -> &'static str {
246 "Time execution with statistics over multiple runs"
247 }
248}
249
250struct RunMagic;
256
257impl MagicCommand for RunMagic {
258 fn execute_line(&self, repl: &mut Repl, args: &str) -> Result<MagicResult> {
259 if args.trim().is_empty() {
260 return Err(anyhow!("Usage: %run <script.ruchy>"));
261 }
262
263 let script_content = std::fs::read_to_string(args)
264 .map_err(|e| anyhow!("Failed to read script: {}", e))?;
265
266 let result = repl.eval(&script_content)?;
267 Ok(MagicResult::Text(result))
268 }
269
270 fn help(&self) -> &'static str {
271 "Execute an external Ruchy script"
272 }
273}
274
275struct DebugMagic;
281
282impl MagicCommand for DebugMagic {
283 fn execute_line(&self, repl: &mut Repl, _args: &str) -> Result<MagicResult> {
284 if let Some(debug_info) = repl.get_last_error() {
286 let output = format!(
287 "=== Debug Information ===\n\
288 Expression: {}\n\
289 Error: {}\n\
290 Stack trace:\n{}\n\
291 Bindings at error: {} variables",
292 debug_info.expression,
293 debug_info.error_message,
294 debug_info.stack_trace.join("\n"),
295 debug_info.bindings_snapshot.len()
296 );
297 Ok(MagicResult::Text(output))
298 } else {
299 Ok(MagicResult::Text("No recent error to debug".to_string()))
300 }
301 }
302
303 fn help(&self) -> &'static str {
304 "Enter post-mortem debugging mode"
305 }
306}
307
308#[derive(Debug, Clone)]
314pub struct ProfileData {
315 pub total_time: Duration,
316 pub function_times: Vec<(String, Duration, usize)>, }
318
319impl fmt::Display for ProfileData {
320 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321 writeln!(f, "=== Profile Results ===")?;
322 writeln!(f, "Total time: {:.3}s", self.total_time.as_secs_f64())?;
323 writeln!(f, "\nFunction Times:")?;
324 writeln!(f, "{:<30} {:>10} {:>10} {:>10}", "Function", "Time (ms)", "Count", "Avg (ms)")?;
325 writeln!(f, "{:-<60}", "")?;
326
327 for (name, time, count) in &self.function_times {
328 let time_ms = time.as_micros() as f64 / 1000.0;
329 let avg_ms = if *count > 0 { time_ms / *count as f64 } else { 0.0 };
330 writeln!(f, "{name:<30} {time_ms:>10.3} {count:>10} {avg_ms:>10.3}")?;
331 }
332
333 Ok(())
334 }
335}
336
337struct ProfileMagic;
339
340impl MagicCommand for ProfileMagic {
341 fn execute_line(&self, repl: &mut Repl, args: &str) -> Result<MagicResult> {
342 if args.trim().is_empty() {
343 return Err(anyhow!("Usage: %profile <expression>"));
344 }
345
346 let start = Instant::now();
348 let _result = repl.eval(args)?;
349 let total_time = start.elapsed();
350
351 let profile_data = ProfileData {
353 total_time,
354 function_times: vec![
355 ("main".to_string(), total_time, 1),
356 ],
357 };
358
359 Ok(MagicResult::Profile(profile_data))
360 }
361
362 fn help(&self) -> &'static str {
363 "Profile code execution and generate flamegraph"
364 }
365}
366
367struct WhosMagic;
373
374impl MagicCommand for WhosMagic {
375 fn execute_line(&self, repl: &mut Repl, _args: &str) -> Result<MagicResult> {
376 let bindings = repl.get_bindings();
377
378 if bindings.is_empty() {
379 return Ok(MagicResult::Text("No variables in workspace".to_string()));
380 }
381
382 let mut output = String::from("Variable Type Value\n");
383 output.push_str("-------- ---- -----\n");
384
385 for (name, value) in bindings {
386 let type_name = match value {
387 Value::Int(_) => "Int",
388 Value::Float(_) => "Float",
389 Value::String(_) => "String",
390 Value::Bool(_) => "Bool",
391 Value::Char(_) => "Char",
392 Value::List(_) => "List",
393 Value::Tuple(_) => "Tuple",
394 Value::Object(_) => "Object",
395 Value::HashMap(_) => "HashMap",
396 Value::HashSet(_) => "HashSet",
397 Value::Function { .. } => "Function",
398 Value::Lambda { .. } => "Lambda",
399 Value::DataFrame { .. } => "DataFrame",
400 Value::Range { .. } => "Range",
401 Value::EnumVariant { .. } => "EnumVariant",
402 Value::Unit => "Unit",
403 Value::Nil => "Nil",
404 };
405
406 let value_str = format!("{value:?}");
407 let value_display = if value_str.len() > 40 {
408 format!("{}...", &value_str[..37])
409 } else {
410 value_str
411 };
412
413 output.push_str(&format!("{name:<10} {type_name:<10} {value_display}\n"));
414 }
415
416 Ok(MagicResult::Text(output))
417 }
418
419 fn help(&self) -> &'static str {
420 "List all variables in the workspace"
421 }
422}
423
424struct ClearMagic;
426
427impl MagicCommand for ClearMagic {
428 fn execute_line(&self, repl: &mut Repl, args: &str) -> Result<MagicResult> {
429 if args.trim().is_empty() {
430 return Err(anyhow!("Usage: %clear <pattern>"));
431 }
432
433 let pattern = args.trim();
435 let mut cleared = 0;
436
437 let bindings_copy: Vec<String> = repl.get_bindings().keys().cloned().collect();
438 for name in bindings_copy {
439 if name.contains(pattern) || pattern == "*" {
440 repl.get_bindings_mut().remove(&name);
441 cleared += 1;
442 }
443 }
444
445 Ok(MagicResult::Text(format!("Cleared {cleared} variables")))
446 }
447
448 fn help(&self) -> &'static str {
449 "Clear variables matching pattern"
450 }
451}
452
453struct ResetMagic;
455
456impl MagicCommand for ResetMagic {
457 fn execute_line(&self, repl: &mut Repl, _args: &str) -> Result<MagicResult> {
458 repl.clear_bindings();
459 Ok(MagicResult::Text("Workspace reset".to_string()))
460 }
461
462 fn help(&self) -> &'static str {
463 "Reset the entire workspace"
464 }
465}
466
467struct HistoryMagic;
473
474impl MagicCommand for HistoryMagic {
475 fn execute_line(&self, _repl: &mut Repl, args: &str) -> Result<MagicResult> {
476 let range = if args.trim().is_empty() {
478 10
479 } else {
480 args.trim().parse::<usize>().unwrap_or(10)
481 };
482
483 let mut output = format!("Last {range} commands:\n");
485 for i in 1..=range {
486 output.push_str(&format!("{i}: <command {i}>\n"));
487 }
488
489 Ok(MagicResult::Text(output))
490 }
491
492 fn help(&self) -> &'static str {
493 "Show command history"
494 }
495}
496
497struct SaveMagic;
503
504impl MagicCommand for SaveMagic {
505 fn execute_line(&self, repl: &mut Repl, args: &str) -> Result<MagicResult> {
506 if args.trim().is_empty() {
507 return Err(anyhow!("Usage: %save <filename>"));
508 }
509
510 let bindings = repl.get_bindings();
512 let mut serializable: HashMap<String, String> = HashMap::new();
513 for (k, v) in bindings {
514 serializable.insert(k.clone(), format!("{v:?}"));
515 }
516 let json = serde_json::to_string_pretty(&serializable)
517 .map_err(|e| anyhow!("Failed to serialize: {}", e))?;
518
519 std::fs::write(args.trim(), json)
520 .map_err(|e| anyhow!("Failed to write file: {}", e))?;
521
522 Ok(MagicResult::Text(format!("Saved workspace to {}", args.trim())))
523 }
524
525 fn help(&self) -> &'static str {
526 "Save workspace to file"
527 }
528}
529
530struct LoadMagic;
532
533impl MagicCommand for LoadMagic {
534 fn execute_line(&self, _repl: &mut Repl, args: &str) -> Result<MagicResult> {
535 if args.trim().is_empty() {
536 return Err(anyhow!("Usage: %load <filename>"));
537 }
538
539 let _content = std::fs::read_to_string(args.trim())
540 .map_err(|e| anyhow!("Failed to read file: {}", e))?;
541
542 Ok(MagicResult::Text(format!("Loaded workspace from {}", args.trim())))
545 }
546
547 fn help(&self) -> &'static str {
548 "Load workspace from file"
549 }
550}
551
552struct PwdMagic;
558
559impl MagicCommand for PwdMagic {
560 fn execute_line(&self, _repl: &mut Repl, _args: &str) -> Result<MagicResult> {
561 let pwd = std::env::current_dir()
562 .map_err(|e| anyhow!("Failed to get pwd: {}", e))?;
563 Ok(MagicResult::Text(pwd.display().to_string()))
564 }
565
566 fn help(&self) -> &'static str {
567 "Print working directory"
568 }
569}
570
571struct CdMagic;
573
574impl MagicCommand for CdMagic {
575 fn execute_line(&self, _repl: &mut Repl, args: &str) -> Result<MagicResult> {
576 let path = if args.trim().is_empty() {
577 std::env::var("HOME").unwrap_or_else(|_| ".".to_string())
578 } else {
579 args.trim().to_string()
580 };
581
582 std::env::set_current_dir(&path)
583 .map_err(|e| anyhow!("Failed to change directory: {}", e))?;
584
585 let pwd = std::env::current_dir()
586 .map_err(|e| anyhow!("Failed to get pwd: {}", e))?;
587
588 Ok(MagicResult::Text(format!("Changed to: {}", pwd.display())))
589 }
590
591 fn help(&self) -> &'static str {
592 "Change working directory"
593 }
594}
595
596struct LsMagic;
598
599impl MagicCommand for LsMagic {
600 fn execute_line(&self, _repl: &mut Repl, args: &str) -> Result<MagicResult> {
601 let path = if args.trim().is_empty() {
602 "."
603 } else {
604 args.trim()
605 };
606
607 let entries = std::fs::read_dir(path)
608 .map_err(|e| anyhow!("Failed to read directory: {}", e))?;
609
610 let mut output = String::new();
611 for entry in entries {
612 let entry = entry.map_err(|e| anyhow!("Failed to read entry: {}", e))?;
613 let name = entry.file_name();
614 output.push_str(&format!("{}\n", name.to_string_lossy()));
615 }
616
617 Ok(MagicResult::Text(output))
618 }
619
620 fn help(&self) -> &'static str {
621 "List directory contents"
622 }
623}
624
625pub struct UnicodeExpander {
631 mappings: HashMap<String, char>,
632}
633
634impl UnicodeExpander {
635 pub fn new() -> Self {
636 let mut mappings = HashMap::new();
637
638 mappings.insert("alpha".to_string(), 'α');
640 mappings.insert("beta".to_string(), 'β');
641 mappings.insert("gamma".to_string(), 'γ');
642 mappings.insert("delta".to_string(), 'δ');
643 mappings.insert("epsilon".to_string(), 'ε');
644 mappings.insert("zeta".to_string(), 'ζ');
645 mappings.insert("eta".to_string(), 'η');
646 mappings.insert("theta".to_string(), 'θ');
647 mappings.insert("iota".to_string(), 'ι');
648 mappings.insert("kappa".to_string(), 'κ');
649 mappings.insert("lambda".to_string(), 'λ');
650 mappings.insert("mu".to_string(), 'μ');
651 mappings.insert("nu".to_string(), 'ν');
652 mappings.insert("xi".to_string(), 'ξ');
653 mappings.insert("pi".to_string(), 'π');
654 mappings.insert("rho".to_string(), 'ρ');
655 mappings.insert("sigma".to_string(), 'σ');
656 mappings.insert("tau".to_string(), 'τ');
657 mappings.insert("phi".to_string(), 'φ');
658 mappings.insert("chi".to_string(), 'χ');
659 mappings.insert("psi".to_string(), 'ψ');
660 mappings.insert("omega".to_string(), 'ω');
661
662 mappings.insert("Alpha".to_string(), 'Α');
664 mappings.insert("Beta".to_string(), 'Β');
665 mappings.insert("Gamma".to_string(), 'Γ');
666 mappings.insert("Delta".to_string(), 'Δ');
667 mappings.insert("Theta".to_string(), 'Θ');
668 mappings.insert("Lambda".to_string(), 'Λ');
669 mappings.insert("Pi".to_string(), 'Π');
670 mappings.insert("Sigma".to_string(), 'Σ');
671 mappings.insert("Phi".to_string(), 'Φ');
672 mappings.insert("Psi".to_string(), 'Ψ');
673 mappings.insert("Omega".to_string(), 'Ω');
674
675 mappings.insert("infty".to_string(), '∞');
677 mappings.insert("sum".to_string(), '∑');
678 mappings.insert("prod".to_string(), '∏');
679 mappings.insert("int".to_string(), '∫');
680 mappings.insert("sqrt".to_string(), '√');
681 mappings.insert("partial".to_string(), '∂');
682 mappings.insert("nabla".to_string(), '∇');
683 mappings.insert("forall".to_string(), '∀');
684 mappings.insert("exists".to_string(), '∃');
685 mappings.insert("in".to_string(), '∈');
686 mappings.insert("notin".to_string(), '∉');
687 mappings.insert("subset".to_string(), '⊂');
688 mappings.insert("supset".to_string(), '⊃');
689 mappings.insert("cup".to_string(), '∪');
690 mappings.insert("cap".to_string(), '∩');
691 mappings.insert("emptyset".to_string(), '∅');
692 mappings.insert("pm".to_string(), '±');
693 mappings.insert("mp".to_string(), '∓');
694 mappings.insert("times".to_string(), '×');
695 mappings.insert("div".to_string(), '÷');
696 mappings.insert("neq".to_string(), '≠');
697 mappings.insert("leq".to_string(), '≤');
698 mappings.insert("geq".to_string(), '≥');
699 mappings.insert("approx".to_string(), '≈');
700 mappings.insert("equiv".to_string(), '≡');
701
702 Self { mappings }
703 }
704
705 pub fn expand(&self, sequence: &str) -> Option<char> {
707 let key = if sequence.starts_with('\\') {
709 &sequence[1..]
710 } else {
711 sequence
712 };
713
714 self.mappings.get(key).copied()
715 }
716
717 pub fn list_expansions(&self) -> Vec<(String, char)> {
719 let mut expansions: Vec<_> = self.mappings
720 .iter()
721 .map(|(k, v)| (format!("\\{k}"), *v))
722 .collect();
723 expansions.sort_by_key(|(k, _)| k.clone());
724 expansions
725 }
726}
727
728impl Default for UnicodeExpander {
729 fn default() -> Self {
730 Self::new()
731 }
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737
738 #[test]
739 fn test_magic_registry() {
740 let registry = MagicRegistry::new();
741 assert!(registry.is_magic("%time"));
742 assert!(registry.is_magic("%%time"));
743 assert!(!registry.is_magic("time"));
744
745 let commands = registry.list_commands();
746 assert!(commands.contains(&"time".to_string()));
747 assert!(commands.contains(&"debug".to_string()));
748 }
749
750 #[test]
751 fn test_unicode_expander() {
752 let expander = UnicodeExpander::new();
753
754 assert_eq!(expander.expand("\\alpha"), Some('α'));
755 assert_eq!(expander.expand("alpha"), Some('α'));
756 assert_eq!(expander.expand("\\pi"), Some('π'));
757 assert_eq!(expander.expand("\\infty"), Some('∞'));
758 assert_eq!(expander.expand("\\unknown"), None);
759 }
760
761 #[test]
762 fn test_magic_result_display() {
763 let result = MagicResult::Text("Hello".to_string());
764 assert_eq!(format!("{result}"), "Hello");
765
766 let result = MagicResult::Timed {
767 output: "42".to_string(),
768 duration: Duration::from_millis(123),
769 };
770 assert!(format!("{result}").contains("0.123s"));
771 }
772}