use eframe::egui; use myc::ast::compiler::emitter; use myc::ast::environment::Environment; use myc::ast::parser::Parser; use std::path::PathBuf; fn main() -> eframe::Result { let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default().with_inner_size([1024.0, 768.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(); let env = Environment::new(); 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(""), trace_logs: Vec::new(), active_tab: AppTab::Output, debug_enabled: false, env, is_first_frame: true, examples, } } } impl CompilerApp { fn compile(&mut self) { let start_total = std::time::Instant::now(); self.trace_logs.clear(); // Reset environment for a fresh run self.env = Environment::new(); self.env.optimization = true; let res = (|| -> Result<(myc::ast::types::Value, std::time::Duration), String> { let start_run = std::time::Instant::now(); let result = if self.debug_enabled { let (res, logs) = self.env.run_debug(&self.source_code)?; // Batch-truncate logs for GUI performance let mut display_logs = logs; if display_logs.len() > 1000 { display_logs.truncate(1000); display_logs.push("... [Trace truncated to 1,000 entries] ...".to_string()); } for line in &mut display_logs { if line.len() > 255 && let Some((idx, _)) = line.char_indices().nth(255) { line.truncate(idx); line.push_str("..."); } } self.trace_logs = display_logs; self.active_tab = AppTab::Trace; res? } else { self.env.run_script(&self.source_code)? }; let duration_run = start_run.elapsed(); Ok((result, duration_run)) })(); match res { Ok((val, d_run)) => { let d_total = start_total.elapsed(); let now = chrono::Local::now().format("%H:%M:%S").to_string(); let mut stats = format!( "Execution Successful.\nResult: {}\n\nTotal Duration: {:?}\nFinished at: {}", val, d_total, now, ); if !self.debug_enabled { stats.push_str(&format!("\nRun Phase: {:?}", d_run)); } self.output_log = stats; 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) { self.env = Environment::new(); self.env.optimization = true; // Enable optimization for AST dump 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 roundtrip(&mut self) { let mut parser = Parser::new(&self.source_code); let ast = parser.parse_expression(); if parser.diagnostics.has_errors() { let errors: Vec<_> = parser .diagnostics .items .iter() .map(|d| d.message.clone()) .collect(); self.output_log = format!("Roundtrip failed — parse errors:\n{}", errors.join("\n")); self.active_tab = AppTab::Output; return; } self.source_code = emitter::emit(&ast); self.output_log = "Roundtrip complete — source reformatted.".to_string(); self.active_tab = AppTab::Output; } 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::utils::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, only_current: bool) { use myc::utils::tester; let filter = if only_current { self.current_example_path .as_ref() .and_then(|p| p.file_name()) .map(|n| n.to_string_lossy().to_string()) } else { None }; 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 = format!( "{}...\n\n", match (update, only_current) { (true, true) => "UPDATING BASELINE FOR CURRENT SCRIPT", (true, false) => "UPDATING ALL BASELINES", (false, _) => "RUNNING BENCHMARKS", } ); } let results = tester::run_benchmarks(update, filter.as_deref()); 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) { // Top Panel: Menu Bar & Toolbar egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { egui::MenuBar::new().ui(ui, |ui: &mut egui::Ui| { ui.menu_button("File", |ui: &mut egui::Ui| { let file_name = self .current_example_path .as_ref() .map(|p| { p.file_name() .unwrap_or_default() .to_string_lossy() .into_owned() }) .unwrap_or_else(|| "None".to_string()); if ui.button(format!("Save {} (Ctrl+S)", file_name)).clicked() { self.save(); ui.close(); } if ui.button("Save As...").clicked() { self.show_save_as = true; ui.close(); } }); ui.menu_button("Build", |ui: &mut egui::Ui| { if ui.button("Compile (Shift+Enter)").clicked() { self.compile(); ui.close(); } if ui.button("Dump AST").clicked() { self.dump_ast(); ui.close(); } if ui.button("Format (Roundtrip)").clicked() { self.roundtrip(); ui.close(); } ui.separator(); ui.checkbox(&mut self.debug_enabled, "Debug Mode"); }); ui.menu_button("Tools", |ui: &mut egui::Ui| { if ui.button("Test All").clicked() { self.run_all_tests(); ui.close(); } ui.separator(); let is_debug = cfg!(debug_assertions); ui.add_enabled_ui(!is_debug, |ui: &mut egui::Ui| { let btn = ui.button("Run All Benchmarks"); if is_debug { btn.on_hover_text("Benchmarks are only available in Release builds."); } else if btn.clicked() { self.run_benchmarks(false, false); ui.close(); } let btn_base_all = ui.button("Update All Baselines"); if is_debug { btn_base_all.on_hover_text( "Updating baselines is only allowed in Release builds.", ); } else if btn_base_all.clicked() { self.run_benchmarks(true, false); ui.close(); } let btn_base_curr = ui.button("Update Current Baseline"); if is_debug { btn_base_curr.on_hover_text( "Updating baselines is only allowed in Release builds.", ); } else if btn_base_curr.clicked() { self.run_benchmarks(true, true); ui.close(); } }); }); }); ui.separator(); // Toolbar (Speed Buttons) ui.horizontal(|ui| { ui.style_mut().spacing.item_spacing.x = 4.0; // tighter spacing for speed buttons if ui .button("▶ Run") .on_hover_text("Compile and Run (Shift+Enter)") .clicked() { self.compile(); } if ui .button("💾 Save") .on_hover_text("Save current file (Ctrl+S)") .clicked() { self.save(); } ui.separator(); if ui .button("🔍 AST") .on_hover_text("Dump Bound AST") .clicked() { self.dump_ast(); } if ui .button("🔄 Format") .on_hover_text("Format/Roundtrip source code") .clicked() { self.roundtrip(); } ui.separator(); ui.toggle_value(&mut self.debug_enabled, "🐛 Debug"); ui.separator(); if ui .button("🧪 Test") .on_hover_text("Run all functional tests") .clicked() { self.run_all_tests(); } ui.separator(); let is_debug = cfg!(debug_assertions); ui.add_enabled_ui(!is_debug, |ui| { let bench_btn = ui.button("⏱ Bench"); if is_debug { bench_btn.on_hover_text("Benchmarks are only available in Release builds."); } else if bench_btn.clicked() { self.run_benchmarks(false, false); } }); }); ui.add_space(2.0); }); // 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); egui::ScrollArea::vertical().show(ui, |ui| { ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| { 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(100.0) .default_height(200.0) .max_height(ctx.available_rect().height() * 0.5) .show(ctx, |ui| { ui.add_space(5.0); if self.show_save_as { ui.group(|ui| { 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); } if ui.button("Clear Log").clicked() { self.output_log.clear(); self.trace_logs.clear(); } }); ui.add_space(5.0); // Log Output Area match self.active_tab { AppTab::Output => { egui::ScrollArea::both() .id_salt("output_log_scroll") .auto_shrink(false) .show(ui, |ui| { ui.add( egui::TextEdit::multiline(&mut self.output_log) .font(egui::TextStyle::Monospace) .hint_text("Ready to compile...") .code_editor() .lock_focus(false) .desired_width(f32::INFINITY) .frame(false), ); }); } AppTab::Trace => { egui::ScrollArea::both() .id_salt("trace_scroll") .auto_shrink(false) .show(ui, |ui| { ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { for line in &self.trace_logs { ui.label(line); } }); }); } } }); // 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; } let mut layouter = |ui: &egui::Ui, buffer: &dyn egui::TextBuffer, _wrap_width: f32| { let mut layout_job = highlight_myc(ui.ctx(), buffer.as_str()); layout_job.wrap.max_width = f32::INFINITY; // Disable wrapping for code editor ui.painter().layout_job(layout_job) }; egui::ScrollArea::both() .id_salt("source_editor_scroll") .auto_shrink(false) .show(ui, |ui| { ui.add( egui::TextEdit::multiline(&mut self.source_code) .id(editor_id) .font(egui::TextStyle::Monospace) .code_editor() .lock_focus(true) .desired_width(f32::INFINITY) .frame(false) .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 }