94fc6bf56d
This commit introduces support for AST macros and templates, enabling users to define and use custom, reusable components within the visual DSL. Key changes include: - A new `MacroRegistry` to manage macro definitions. - A `MacroExpander` to process macro calls and expand templates. - A `MacroEvaluator` trait for evaluating expressions during expansion. - The `Expansion` node in `BoundKind` to preserve the original macro call and its expanded form for debugging. - Updates to the `Binder`, `Dumper`, and `UpvalueAnalyzer` to handle the new macro constructs. - New examples demonstrating various macro functionalities like `unless`, splicing, and nested macros.
521 lines
19 KiB
Rust
521 lines
19 KiB
Rust
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<PathBuf>,
|
|
save_as_name: String,
|
|
show_save_as: bool,
|
|
output_log: String,
|
|
trace_logs: Vec<String>,
|
|
active_tab: AppTab,
|
|
debug_enabled: bool,
|
|
env: Environment,
|
|
is_first_frame: bool,
|
|
examples: Vec<Example>,
|
|
}
|
|
|
|
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
|
|
}
|