use eframe::egui; use myc::ast::environment::Environment; use std::path::PathBuf; fn main() -> eframe::Result { let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default().with_inner_size([800.0, 600.0]), ..Default::default() }; eframe::run_native( "Compiler GUI", options, Box::new(|_cc| Ok(Box::new(CompilerApp::default()))), ) } struct Example { name: String, path: PathBuf, content: String, } #[derive(PartialEq)] enum AppTab { Output, Trace, } struct CompilerApp { source_code: String, current_example_path: Option, save_as_name: String, show_save_as: bool, output_log: String, trace_logs: Vec, active_tab: AppTab, debug_enabled: bool, env: Environment, is_first_frame: bool, examples: Vec, } impl Default for CompilerApp { fn default() -> Self { let examples = std::fs::read_dir("examples") .map(|entries| { entries .filter_map(|e| e.ok()) .filter(|e| e.path().extension().is_some_and(|ext| ext == "myc")) .filter_map(|e| { let path = e.path(); let name = e.file_name().into_string().ok()?; let content = std::fs::read_to_string(&path).ok()?; Some(Example { name, path, content }) }) .collect() }) .unwrap_or_default(); Self { source_code: String::from(r#" (do (def fib (fn [n] (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2)))))) (fib 10) ) "#), current_example_path: None, save_as_name: String::new(), show_save_as: false, output_log: String::from("Ready to compile..."), trace_logs: Vec::new(), active_tab: AppTab::Output, debug_enabled: false, env: Environment::new(), is_first_frame: true, examples, } } } impl CompilerApp { fn compile(&mut self) { let start_total = std::time::Instant::now(); self.trace_logs.clear(); let res = (|| -> Result<(myc::ast::types::Value, std::time::Duration, std::time::Duration, std::time::Duration), String> { let start_compile = std::time::Instant::now(); let compiled = self.env.compile(&self.source_code)?; let duration_compile = start_compile.elapsed(); let start_link = std::time::Instant::now(); let linked = self.env.link(compiled); let duration_link = start_link.elapsed(); let start_run = std::time::Instant::now(); let result = if self.debug_enabled { let mut vm = myc::ast::vm::VM::new(self.env.global_values.clone()); let mut observer = myc::ast::vm::TracingObserver::new(); let res = vm.run_with_observer(&mut observer, &linked); self.trace_logs = observer.logs; self.active_tab = AppTab::Trace; res? } else { self.env.run(&linked)? }; let duration_run = start_run.elapsed(); Ok((result, duration_compile, duration_link, duration_run)) })(); match res { Ok((val, d_compile, d_link, d_run)) => { let d_total = start_total.elapsed(); let now = chrono::Local::now().format("%H:%M:%S").to_string(); self.output_log = format!( "Execution Successful.\nResult: {}\n\nPhases:\n - Compile: {:?}\n - Link: {:?}\n - Run: {:?}\n\nTotal Duration: {:?}\nFinished at: {}", val, d_compile, d_link, d_run, d_total, now, ); if !self.debug_enabled { self.active_tab = AppTab::Output; } } Err(e) => { self.output_log = format!("Error during Compilation/Execution: {}", e); self.active_tab = AppTab::Output; } } } fn dump_ast(&mut self) { match self.env.dump_ast(&self.source_code) { Ok(dump) => { self.output_log = format!( "--- BOUND AST DUMP ---\n\n{}", dump ); } Err(e) => { self.output_log = format!("Error during AST Binding: {}", e); } } } fn save(&mut self) { if let Some(path) = &self.current_example_path { match std::fs::write(path, &self.source_code) { Ok(_) => { self.output_log = format!("Successfully saved to {:?}", path); // Update the cached content in examples list so it stays in sync if let Some(ex) = self.examples.iter_mut().find(|e| e.path == *path) { ex.content = self.source_code.clone(); } } Err(e) => { self.output_log = format!("Failed to save: {}", e); } } } else { self.output_log = "No file loaded to save. Use 'Examples' to load one.".to_string(); } } fn save_as(&mut self) { if self.save_as_name.trim().is_empty() { self.output_log = "Error: Please enter a filename.".to_string(); return; } let mut filename = self.save_as_name.trim().to_string(); if !filename.ends_with(".myc") { filename.push_str(".myc"); } let path = PathBuf::from("examples").join(filename); match std::fs::write(&path, &self.source_code) { Ok(_) => { self.output_log = format!("Successfully saved new file to {:?}", path); self.current_example_path = Some(path); self.show_save_as = false; self.save_as_name.clear(); self.refresh_examples(); } Err(e) => { self.output_log = format!("Failed to save: {}", e); } } } fn refresh_examples(&mut self) { if let Ok(entries) = std::fs::read_dir("examples") { self.examples = entries .filter_map(|e| e.ok()) .filter(|e| e.path().extension().is_some_and(|ext| ext == "myc")) .filter_map(|e| { let path = e.path(); let name = e.file_name().into_string().ok()?; let content = std::fs::read_to_string(&path).ok()?; Some(Example { name, path, content }) }) .collect(); } } fn run_all_tests(&mut self) { use myc::ast::tester; let results = tester::run_functional_tests(); let mut log = String::from("FUNCTIONAL TEST RESULTS:\n\n"); let mut passed = 0; for res in &results { log.push_str(&format!("{}: {} - {}\n", if res.success { "✅" } else { "❌" }, res.name, res.message)); if res.success { passed += 1; } } log.push_str(&format!("\nTotal: {}/{}", passed, results.len())); self.output_log = log; } fn run_benchmarks(&mut self, update: bool) { use myc::ast::tester; if cfg!(debug_assertions) && !update { self.output_log = String::from("⚠️ WARNING: Benchmarks in Debug mode are inaccurate!\nUse Release build for real measurements.\n\n"); } else { self.output_log = String::from(if update { "UPDATING BASELINES...\n\n" } else { "RUNNING BENCHMARKS...\n\n" }); } let results = tester::run_benchmarks(update); let mut log = self.output_log.clone(); for res in results { let diff = res.diff_pct.map_or(String::new(), |d| format!(" ({:+.1}%)", d)); log.push_str(&format!("{}: {} - {:?}{}\n", res.status, res.name, res.median, diff)); } self.output_log = log; } } impl eframe::App for CompilerApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { // Left Side Panel: Examples egui::SidePanel::left("examples_panel") .resizable(true) .default_width(150.0) .show(ctx, |ui| { ui.heading("Examples"); ui.add_space(5.0); for example in &self.examples { if ui.button(&example.name).clicked() { self.source_code = example.content.clone(); self.current_example_path = Some(example.path.clone()); } } }); // Bottom Panel: Output Log & Controls (Resizable) egui::TopBottomPanel::bottom("bottom_panel") .resizable(true) .min_height(150.0) .default_height(250.0) .show(ctx, |ui| { ui.add_space(5.0); // Compile Button (at the top of the bottom panel) ui.horizontal(|ui| { if ui.button("Compile (Shift+Enter)").clicked() { self.compile(); } ui.checkbox(&mut self.debug_enabled, "Debug Mode"); if ui.button("Dump AST").clicked() { self.dump_ast(); } if let Some(path) = &self.current_example_path { let file_name = path.file_name().unwrap_or_default().to_string_lossy(); if ui.button(format!("Save {} (Ctrl+S)", file_name)).clicked() { self.save(); } } else { ui.add_enabled_ui(false, |ui| { let _ = ui.button("Save (None loaded)"); }); } if ui.button("Save As...").clicked() { self.show_save_as = !self.show_save_as; } if ui.button("Test All").clicked() { self.run_all_tests(); } let is_debug = cfg!(debug_assertions); ui.add_enabled_ui(!is_debug, |ui| { let btn = ui.button("Run Benchmarks"); if is_debug { btn.on_hover_text("Benchmarks are only available in Release builds for accuracy."); } else if btn.clicked() { self.run_benchmarks(false); } }); ui.add_enabled_ui(!is_debug, |ui| { let btn = ui.button("Update Baselines"); if is_debug { btn.on_hover_text("Updating baselines is only allowed in Release builds."); } else if btn.clicked() { self.run_benchmarks(true); } }); }); if self.show_save_as { ui.horizontal(|ui| { ui.label("New Filename:"); ui.text_edit_singleline(&mut self.save_as_name); if ui.button("Save Now").clicked() { self.save_as(); } if ui.button("Cancel").clicked() { self.show_save_as = false; } }); ui.add_space(5.0); } ui.horizontal(|ui| { ui.selectable_value(&mut self.active_tab, AppTab::Output, "Output"); ui.selectable_value(&mut self.active_tab, AppTab::Trace, "Execution Trace"); ui.add_space(20.0); if ui.button("Copy Content").clicked() { let text = match self.active_tab { AppTab::Output => self.output_log.clone(), AppTab::Trace => self.trace_logs.join("\n"), }; ui.ctx().copy_text(text); } }); // Log Output Area egui::ScrollArea::vertical() .id_salt("output_log_scroll") .show(ui, |ui| { match self.active_tab { AppTab::Output => { ui.add_sized( ui.available_size(), egui::TextEdit::multiline(&mut self.output_log) .font(egui::TextStyle::Monospace) .desired_width(f32::INFINITY) .interactive(true) ); }, AppTab::Trace => { let mut combined = self.trace_logs.join("\n"); let mut layouter = |ui: &egui::Ui, buffer: &dyn egui::TextBuffer, _wrap_width: f32| { let layout_job = highlight_myc(ui.ctx(), buffer.as_str()); ui.painter().layout_job(layout_job) }; ui.add_sized( ui.available_size(), egui::TextEdit::multiline(&mut combined) .font(egui::TextStyle::Monospace) .desired_width(f32::INFINITY) .interactive(false) .layouter(&mut layouter) ); } } }); }); // Central Panel: Source Code Editor (Fills remaining space) egui::CentralPanel::default().show(ctx, |ui| { ui.heading("Source Code Editor"); ui.label("Input Source:"); let editor_id = ui.id().with("source_code_editor"); // Handle Shift+Enter shortcut BEFORE the editor handles it let compile_shortcut = egui::KeyboardShortcut::new(egui::Modifiers::SHIFT, egui::Key::Enter); if ui.input_mut(|i| i.consume_shortcut(&compile_shortcut)) { self.compile(); } let save_shortcut = egui::KeyboardShortcut::new(egui::Modifiers::COMMAND, egui::Key::S); if ui.input_mut(|i| i.consume_shortcut(&save_shortcut)) { self.save(); } // Set initial focus if self.is_first_frame { ui.ctx().memory_mut(|m| m.request_focus(editor_id)); self.is_first_frame = false; } egui::ScrollArea::vertical() .id_salt("source_code_scroll") .show(ui, |ui| { let mut layouter = |ui: &egui::Ui, buffer: &dyn egui::TextBuffer, _wrap_width: f32| { let layout_job = highlight_myc(ui.ctx(), buffer.as_str()); ui.painter().layout_job(layout_job) }; ui.add_sized( ui.available_size(), egui::TextEdit::multiline(&mut self.source_code) .id(editor_id) .font(egui::TextStyle::Monospace) .desired_width(f32::INFINITY) .lock_focus(true) .layouter(&mut layouter), ); }); }); } } fn highlight_myc(ctx: &egui::Context, code: &str) -> egui::text::LayoutJob { let mut job = egui::text::LayoutJob::default(); let theme = &ctx.style().visuals; let color_kw = egui::Color32::from_rgb(86, 156, 214); // Blue let color_str = egui::Color32::from_rgb(206, 145, 120); // Orange/Brown let color_num = egui::Color32::from_rgb(181, 206, 168); // Greenish let color_comment = egui::Color32::from_rgb(106, 153, 85); // Green let color_bracket = [ egui::Color32::from_rgb(255, 215, 0), // Gold egui::Color32::from_rgb(218, 112, 214), // Orchid egui::Color32::from_rgb(24, 131, 215), // Blue ]; let mut chars = code.chars().peekable(); let mut bracket_depth = 0; while let Some(c) = chars.next() { let mut text = String::from(c); let mut color = theme.widgets.noninteractive.text_color(); match c { ';' => { while let Some(&next) = chars.peek() { if next == '\n' { break; } text.push(chars.next().unwrap()); } color = color_comment; } '"' => { while let Some(next) = chars.next() { text.push(next); if next == '"' { break; } if next == '\\' && let Some(escaped) = chars.next() { text.push(escaped); } } color = color_str; } ':' => { while let Some(&next) = chars.peek() { if next.is_whitespace() || "()[]{}".contains(next) { break; } text.push(chars.next().unwrap()); } color = color_kw; } '(' | '[' | '{' => { color = color_bracket[bracket_depth % 3]; bracket_depth += 1; } ')' | ']' | '}' => { bracket_depth = bracket_depth.saturating_sub(1); color = color_bracket[bracket_depth % 3]; } '`' | '~' | '@' => { color = color_kw; } '0'..='9' => { while let Some(&next) = chars.peek() { if next.is_ascii_digit() || next == '.' { text.push(chars.next().unwrap()); } else { break; } } color = color_num; } _ if c.is_alphabetic() || "+-*/<=>!$%&_?.".contains(c) => { while let Some(&next) = chars.peek() { if next.is_alphanumeric() || "+-*/<=>!$%&_?.".contains(next) { text.push(chars.next().unwrap()); } else { break; } } if ["fn", "def", "do", "if", "assign", "recur", "cond", "macro"].contains(&text.as_str()) { color = color_kw; } } _ => {} } job.append( &text, 0.0, egui::TextFormat { font_id: egui::FontId::monospace(14.0), color, ..Default::default() }, ); } job }