feat: Implement AST macros and templates

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.
This commit is contained in:
Michael Schimmel
2026-02-18 12:51:07 +01:00
parent 98deb8f3fe
commit 94fc6bf56d
23 changed files with 798 additions and 67 deletions
+73 -36
View File
@@ -1,14 +1,16 @@
use std::rc::Rc;
use std::cell::RefCell;
use std::collections::HashMap;
use crate::ast::types::{Value, StaticType};
use crate::ast::types::{Value, StaticType, Object};
use crate::ast::nodes::{Node, UntypedKind};
use crate::ast::parser::Parser;
use crate::ast::compiler::binder::Binder;
use crate::ast::compiler::BoundKind;
use crate::ast::vm::{VM, TracingObserver};
use crate::ast::compiler::tco::TCO;
use crate::ast::compiler::dumper::Dumper;
use crate::ast::compiler::macros::{MacroExpander, MacroRegistry, MacroEvaluator};
pub struct Environment {
pub global_names: Rc<RefCell<HashMap<String, (u32, StaticType)>>>,
@@ -16,6 +18,30 @@ pub struct Environment {
pub debug_mode: bool,
}
/// Evaluator used during macro expansion to allow compile-time logic.
struct RuntimeMacroEvaluator {
global_names: Rc<RefCell<HashMap<String, (u32, StaticType)>>>,
global_values: Rc<RefCell<Vec<Value>>>,
}
impl MacroEvaluator for RuntimeMacroEvaluator {
fn evaluate(&self, node: &Node<UntypedKind>, bindings: &HashMap<Rc<str>, Node<UntypedKind>>) -> Result<Value, String> {
// 1. Check if it's a simple parameter substitution
if let UntypedKind::Identifier(name) = &node.kind {
if let Some(arg_node) = bindings.get(name) {
return Ok(Value::Object(Rc::new(arg_node.clone()) as Rc<dyn Object>));
}
}
// 2. Full evaluation for complex compile-time expressions
// Note: This evaluator currently doesn't see macro parameters in complex expressions
// unless they are explicitly substituted. For now, we support global-only logic.
let bound_ast = Binder::bind_root(self.global_names.clone(), node)?;
let mut vm = VM::new(self.global_values.clone());
vm.run(&bound_ast)
}
}
impl Default for Environment {
fn default() -> Self {
Self::new()
@@ -37,6 +63,14 @@ impl Environment {
self.debug_mode = enabled;
}
fn get_expander(&self) -> MacroExpander<RuntimeMacroEvaluator> {
let evaluator = RuntimeMacroEvaluator {
global_names: self.global_names.clone(),
global_values: self.global_values.clone(),
};
MacroExpander::new(MacroRegistry::new(), evaluator)
}
pub fn register_native(&self, name: &str, ty: StaticType, func: impl Fn(Vec<Value>) -> Value + 'static) {
let mut names = self.global_names.borrow_mut();
let mut values = self.global_values.borrow_mut();
@@ -114,13 +148,38 @@ impl Environment {
}
pub fn dump_ast(&self, source: &str) -> Result<String, String> {
let compiled = self.compile(source)?;
let linked = self.link(compiled);
Ok(Dumper::dump(&linked))
}
/// Frontend: Parse -> Expand Macros -> Bind & Type Check
pub fn compile(&self, source: &str) -> Result<Node<BoundKind, StaticType>, String> {
// 1. Parse
let mut parser = Parser::new(source)?;
let untyped_ast = parser.parse_expression()?;
let bound_ast = Binder::bind_root(self.global_names.clone(), &untyped_ast)?;
let optimized_ast = TCO::optimize(bound_ast);
Ok(Dumper::dump(&optimized_ast))
// 2. Check for trailing tokens
if !parser.at_eof() {
return Err("Unexpected trailing expressions in script. Use (do ...) for sequences.".to_string());
}
// 3. Expand Macros
let expanded_ast = self.get_expander().expand(untyped_ast)?;
// 4. Bind & Type Check
Binder::bind_root(self.global_names.clone(), &expanded_ast)
}
/// Backend: Optimization (TCO, etc.)
pub fn link(&self, node: Node<BoundKind, StaticType>) -> Node<BoundKind, StaticType> {
TCO::optimize(node)
}
/// Runtime: Execute the linked AST in the VM
pub fn run(&self, node: &Node<BoundKind, StaticType>) -> Result<Value, String> {
let mut vm = VM::new(self.global_values.clone());
vm.run(node)
}
pub fn run_script(&self, source: &str) -> Result<Value, String> {
@@ -131,42 +190,20 @@ impl Environment {
}
res
} else {
// 1. Parse
let mut parser = Parser::new(source)?;
let untyped_ast = parser.parse_expression()?;
// 2. Check for trailing tokens
if !parser.at_eof() {
return Err("Unexpected trailing expressions in script. Use (do ...) for sequences.".to_string());
}
// 3. Bind & Type Check
let bound_ast = Binder::bind_root(self.global_names.clone(), &untyped_ast)?;
// 4. Optimize
let bound_ast = TCO::optimize(bound_ast);
// 5. Execute
let mut vm = VM::new(self.global_values.clone());
vm.run(&bound_ast)
let compiled = self.compile(source)?;
let linked = self.link(compiled);
self.run(&linked)
}
}
pub fn run_debug(&self, source: &str) -> Result<(Result<Value, String>, Vec<String>), String> {
// 1. Parse
let mut parser = Parser::new(source)?;
let untyped_ast = parser.parse_expression()?;
let compiled = self.compile(source)?;
let linked = self.link(compiled);
// 2. Bind
let bound_ast = Binder::bind_root(self.global_names.clone(), &untyped_ast)?;
// 3. Optimize (TCO)
let bound_ast = TCO::optimize(bound_ast);
// 4. Execute with TracingObserver
// Execute with TracingObserver
let mut vm = VM::new(self.global_values.clone());
let mut observer = TracingObserver::new();
let result = vm.run_with_observer(&mut observer, &bound_ast);
let result = vm.run_with_observer(&mut observer, &linked);
Ok((result, observer.logs))
}