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
+13
View File
@@ -300,6 +300,19 @@ impl Binder {
Ok(self.make_node(node.identity.clone(), BoundKind::Map { entries: bound_entries }, ty))
},
UntypedKind::Expansion { call, expanded } => {
let bound_expanded = self.bind(expanded)?;
let ty = bound_expanded.ty.clone();
Ok(self.make_node(node.identity.clone(), BoundKind::Expansion {
original_call: Rc::from(call.as_ref().clone()),
bound_expanded: Box::new(bound_expanded),
}, ty))
},
UntypedKind::Template(_) | UntypedKind::Placeholder(_) | UntypedKind::Splice(_) | UntypedKind::MacroDecl { .. } => {
Err(format!("Macro construct {:?} found in Binder. Macros must be expanded before binding.", node.kind))
}
UntypedKind::Extension(_) => {
Err("Custom extensions not supported in Binder yet".to_string())
+8
View File
@@ -70,6 +70,13 @@ pub enum BoundKind {
Map {
entries: Vec<(Node<BoundKind, StaticType>, Node<BoundKind, StaticType>)>,
},
/// An expanded macro call, preserving the original call for debugging and UI.
Expansion {
/// The original call from the untyped AST.
original_call: Rc<Node<crate::ast::nodes::UntypedKind>>,
/// The result of binding the expanded AST.
bound_expanded: Box<Node<BoundKind, StaticType>>,
},
}
impl BoundKind {
@@ -88,6 +95,7 @@ impl BoundKind {
BoundKind::Block { .. } => "BLOCK".to_string(),
BoundKind::Tuple { elements } => format!("TUPLE({})", elements.len()),
BoundKind::Map { entries } => format!("MAP({})", entries.len()),
BoundKind::Expansion { .. } => "EXPANSION".to_string(),
}
}
}
+7
View File
@@ -149,6 +149,13 @@ impl Dumper {
}
self.indent -= 1;
}
BoundKind::Expansion { original_call, bound_expanded } => {
self.log(&format!("Expansion (Original: {:?})", original_call.kind), node);
self.indent += 1;
self.visit(bound_expanded);
self.indent -= 1;
}
}
}
}
+453
View File
@@ -0,0 +1,453 @@
use std::collections::HashMap;
use std::rc::Rc;
use crate::ast::nodes::{Node, UntypedKind};
use crate::ast::types::{Value, Identity};
/// Trait for evaluating expressions during macro expansion.
/// Similar to TMacroEvaluatorProc in the Delphi implementation.
pub trait MacroEvaluator {
fn evaluate(&self, node: &Node<UntypedKind>, bindings: &HashMap<Rc<str>, Node<UntypedKind>>) -> Result<Value, String>;
}
/// A registry for macro declarations.
pub struct MacroRegistry {
scopes: Vec<HashMap<Rc<str>, (Vec<Rc<str>>, Node<UntypedKind>)>>,
}
impl MacroRegistry {
pub fn new() -> Self {
Self {
scopes: vec![HashMap::new()],
}
}
pub fn push(&mut self) {
self.scopes.push(HashMap::new());
}
pub fn pop(&mut self) {
if self.scopes.len() > 1 {
self.scopes.pop();
}
}
pub fn define(&mut self, name: Rc<str>, params: Vec<Rc<str>>, body: Node<UntypedKind>) {
if let Some(current) = self.scopes.last_mut() {
current.insert(name, (params, body));
}
}
pub fn lookup(&self, name: &str) -> Option<(Vec<Rc<str>>, Node<UntypedKind>)> {
for scope in self.scopes.iter().rev() {
if let Some(m) = scope.get(name) {
return Some(m.clone());
}
}
None
}
}
pub struct MacroExpander<E: MacroEvaluator> {
registry: MacroRegistry,
evaluator: E,
}
impl<E: MacroEvaluator> MacroExpander<E> {
pub fn new(registry: MacroRegistry, evaluator: E) -> Self {
Self { registry, evaluator }
}
pub fn expand(&mut self, node: Node<UntypedKind>) -> Result<Node<UntypedKind>, String> {
self.expand_recursive(node)
}
fn expand_recursive(&mut self, node: Node<UntypedKind>) -> Result<Node<UntypedKind>, String> {
match node.kind {
UntypedKind::MacroDecl { name, params, body } => {
self.registry.define(name, params, *body);
Ok(Node {
identity: node.identity,
kind: UntypedKind::Nop,
ty: (),
})
}
UntypedKind::Call { callee, args } => {
if let UntypedKind::Identifier(ref name) = callee.kind {
if let Some((params, body)) = self.registry.lookup(name) {
let mut expanded_args = Vec::new();
for arg in args {
expanded_args.push(self.expand_recursive(arg)?);
}
let expanded = self.expand_call(node.identity.clone(), name, params, expanded_args, body)?;
return self.expand_recursive(expanded);
}
}
let expanded_callee = self.expand_recursive(*callee)?;
let mut expanded_args = Vec::new();
for arg in args {
expanded_args.push(self.expand_recursive(arg)?);
}
Ok(Node {
identity: node.identity,
kind: UntypedKind::Call {
callee: Box::new(expanded_callee),
args: expanded_args,
},
ty: (),
})
}
UntypedKind::Block { exprs } => {
self.registry.push();
let mut expanded_exprs = Vec::new();
for expr in exprs {
expanded_exprs.push(self.expand_recursive(expr)?);
}
self.registry.pop();
Ok(Node {
identity: node.identity,
kind: UntypedKind::Block { exprs: expanded_exprs },
ty: (),
})
}
UntypedKind::Lambda { params, body } => {
self.registry.push();
let expanded_body = self.expand_recursive(Node::clone(&body))?;
self.registry.pop();
Ok(Node {
identity: node.identity,
kind: UntypedKind::Lambda { params, body: Rc::new(expanded_body) },
ty: (),
})
}
UntypedKind::If { cond, then_br, else_br } => {
let cond = self.expand_recursive(*cond)?;
let then_br = self.expand_recursive(*then_br)?;
let else_br = if let Some(e) = else_br {
Some(Box::new(self.expand_recursive(*e)?))
} else {
None
};
Ok(Node {
identity: node.identity,
kind: UntypedKind::If { cond: Box::new(cond), then_br: Box::new(then_br), else_br },
ty: (),
})
}
UntypedKind::Def { name, value } => {
let value = self.expand_recursive(*value)?;
Ok(Node {
identity: node.identity,
kind: UntypedKind::Def { name, value: Box::new(value) },
ty: (),
})
}
UntypedKind::Assign { target, value } => {
let target = self.expand_recursive(*target)?;
let value = self.expand_recursive(*value)?;
Ok(Node {
identity: node.identity,
kind: UntypedKind::Assign { target: Box::new(target), value: Box::new(value) },
ty: (),
})
}
UntypedKind::Tuple { elements } => {
let mut expanded_elements = Vec::new();
for e in elements {
expanded_elements.push(self.expand_recursive(e)?);
}
Ok(Node {
identity: node.identity,
kind: UntypedKind::Tuple { elements: expanded_elements },
ty: (),
})
}
UntypedKind::Map { entries } => {
let mut expanded_entries = Vec::new();
for (k, v) in entries {
expanded_entries.push((self.expand_recursive(k)?, self.expand_recursive(v)?));
}
Ok(Node {
identity: node.identity,
kind: UntypedKind::Map { entries: expanded_entries },
ty: (),
})
}
UntypedKind::Expansion { call, expanded } => {
let expanded = self.expand_recursive(*expanded)?;
Ok(Node {
identity: node.identity,
kind: UntypedKind::Expansion { call, expanded: Box::new(expanded) },
ty: (),
})
}
_ => Ok(node),
}
}
fn expand_call(&self, identity: Identity, name: &str, params: Vec<Rc<str>>, args: Vec<Node<UntypedKind>>, body: Node<UntypedKind>) -> Result<Node<UntypedKind>, String> {
if params.len() != args.len() {
return Err(format!("Macro {} expects {} arguments, but got {}", name, params.len(), args.len()));
}
let mut bindings = HashMap::new();
for (p, a) in params.into_iter().zip(args.into_iter()) {
bindings.insert(p, a);
}
let expanded_body = if let UntypedKind::Template(inner) = body.kind {
self.expand_template(*inner, &bindings)?
} else {
body
};
let original_call = Node {
identity: identity.clone(),
kind: UntypedKind::Call {
callee: Box::new(Node {
identity: identity.clone(),
kind: UntypedKind::Identifier(Rc::from(name)),
ty: (),
}),
args: bindings.iter().map(|(_, v)| v.clone()).collect(),
},
ty: (),
};
Ok(Node {
identity,
kind: UntypedKind::Expansion {
call: Box::new(original_call),
expanded: Box::new(expanded_body),
},
ty: (),
})
}
fn expand_template(&self, node: Node<UntypedKind>, bindings: &HashMap<Rc<str>, Node<UntypedKind>>) -> Result<Node<UntypedKind>, String> {
match node.kind {
UntypedKind::Placeholder(inner) => {
let val = self.evaluator.evaluate(&inner, bindings)?;
Ok(self.value_to_node(val, node.identity))
}
UntypedKind::Splice(_) => {
Err("Splice node found outside of a container context".to_string())
}
UntypedKind::Tuple { elements } => {
let mut new_elements = Vec::new();
for e in elements {
if let UntypedKind::Splice(ref inner) = e.kind {
let val = self.evaluator.evaluate(inner, bindings)?;
self.handle_splice_value(val, node.identity.clone(), &mut new_elements)?;
} else {
new_elements.push(self.expand_template(e, bindings)?);
}
}
Ok(Node {
identity: node.identity,
kind: UntypedKind::Tuple { elements: new_elements },
ty: (),
})
}
UntypedKind::Block { exprs } => {
let mut new_exprs = Vec::new();
for e in exprs {
if let UntypedKind::Splice(ref inner) = e.kind {
let val = self.evaluator.evaluate(inner, bindings)?;
self.handle_splice_value(val, node.identity.clone(), &mut new_exprs)?;
} else {
new_exprs.push(self.expand_template(e, bindings)?);
}
}
Ok(Node {
identity: node.identity,
kind: UntypedKind::Block { exprs: new_exprs },
ty: (),
})
}
UntypedKind::Map { entries } => {
let mut new_entries = Vec::new();
for (k, v) in entries {
new_entries.push((self.expand_template(k, bindings)?, self.expand_template(v, bindings)?));
}
Ok(Node {
identity: node.identity,
kind: UntypedKind::Map { entries: new_entries },
ty: (),
})
}
UntypedKind::Call { callee, args } => {
let expanded_callee = self.expand_template(*callee, bindings)?;
let mut expanded_args = Vec::new();
for arg in args {
if let UntypedKind::Splice(ref inner) = arg.kind {
let val = self.evaluator.evaluate(inner, bindings)?;
self.handle_splice_value(val, node.identity.clone(), &mut expanded_args)?;
} else {
expanded_args.push(self.expand_template(arg, bindings)?);
}
}
Ok(Node {
identity: node.identity,
kind: UntypedKind::Call { callee: Box::new(expanded_callee), args: expanded_args },
ty: (),
})
}
UntypedKind::If { cond, then_br, else_br } => {
let cond = self.expand_template(*cond, bindings)?;
let then_br = self.expand_template(*then_br, bindings)?;
let else_br = if let Some(e) = else_br {
Some(Box::new(self.expand_template(*e, bindings)?))
} else {
None
};
Ok(Node {
identity: node.identity,
kind: UntypedKind::If { cond: Box::new(cond), then_br: Box::new(then_br), else_br },
ty: (),
})
}
_ => Ok(node),
}
}
fn handle_splice_value(&self, val: Value, _identity: Identity, target: &mut Vec<Node<UntypedKind>>) -> Result<(), String> {
match val {
Value::Object(obj) => {
if let Some(node) = obj.as_any().downcast_ref::<Node<UntypedKind>>() {
match &node.kind {
UntypedKind::Tuple { elements } => {
for e in elements { target.push(e.clone()); }
return Ok(());
}
UntypedKind::Block { exprs } => {
for e in exprs { target.push(e.clone()); }
return Ok(());
}
_ => {}
}
}
Err(format!("Strict Splicing: Expected Tuple or Block node, but found {}", obj.type_name()))
}
_ => Err(format!("Strict Splicing: Expected sequence (Tuple/Block Node), but found Value: {}", val)),
}
}
fn value_to_node(&self, val: Value, identity: Identity) -> Node<UntypedKind> {
match val {
Value::Object(obj) => {
if let Some(node) = obj.as_any().downcast_ref::<Node<UntypedKind>>() {
return node.clone();
}
Node { identity, kind: UntypedKind::Constant(Value::Object(obj)), ty: () }
}
_ => Node { identity, kind: UntypedKind::Constant(val), ty: () }
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::parser::Parser;
use crate::ast::types::Value;
struct SimpleEvaluator;
impl MacroEvaluator for SimpleEvaluator {
fn evaluate(&self, node: &Node<UntypedKind>, bindings: &HashMap<Rc<str>, Node<UntypedKind>>) -> Result<Value, String> {
if let UntypedKind::Identifier(ref name) = node.kind {
if let Some(arg) = bindings.get(name) {
return Ok(Value::Object(Rc::new(arg.clone())));
}
}
// For real expressions, we would need Binder + VM here.
Err(format!("SimpleEvaluator cannot evaluate complex expression: {:?}", node.kind))
}
}
#[test]
fn test_macro_expansion_unless() {
let source = "
(do
(macro unless [c b] `(if ~c void ~b))
(unless true 42))
";
let mut parser = Parser::new(source).unwrap();
let untyped = parser.parse_expression().unwrap();
let mut expander = MacroExpander::new(MacroRegistry::new(), SimpleEvaluator);
let expanded = expander.expand(untyped).unwrap();
if let UntypedKind::Block { exprs } = &expanded.kind {
assert_eq!(exprs.len(), 2);
assert!(matches!(exprs[0].kind, UntypedKind::Nop));
if let UntypedKind::Expansion { call: _, expanded: result } = &exprs[1].kind {
if let UntypedKind::If { cond, then_br, else_br } = &result.kind {
if let UntypedKind::Constant(Value::Bool(b)) = &cond.kind {
assert_eq!(*b, true);
} else { panic!("Expected true, got {:?}", cond.kind); }
assert!(matches!(then_br.kind, UntypedKind::Constant(Value::Void)));
if let Some(eb) = else_br {
if let UntypedKind::Constant(Value::Int(n)) = &eb.kind {
assert_eq!(*n, 42);
} else { panic!("Expected 42, got {:?}", eb.kind); }
} else { panic!("Else branch missing"); }
} else {
panic!("Expansion result should be an IF, got {:?}", result.kind);
}
} else {
panic!("Second expression should be an Expansion, got {:?}", exprs[1].kind);
}
} else {
panic!("Root should be a Block");
}
}
#[test]
fn test_macro_expansion_splice() {
let source = "
(do
(macro wrap [items] `[0 ~@items 4])
(def t (wrap [1 2 3])))
";
let mut parser = Parser::new(source).unwrap();
let untyped = parser.parse_expression().unwrap();
let mut expander = MacroExpander::new(MacroRegistry::new(), SimpleEvaluator);
let expanded = expander.expand(untyped).unwrap();
if let UntypedKind::Block { exprs } = &expanded.kind {
if let UntypedKind::Def { value, .. } = &exprs[1].kind {
if let UntypedKind::Expansion { expanded: result, .. } = &value.kind {
if let UntypedKind::Tuple { elements } = &result.kind {
assert_eq!(elements.len(), 5);
if let UntypedKind::Constant(Value::Int(n)) = &elements[0].kind { assert_eq!(*n, 0); }
if let UntypedKind::Constant(Value::Int(n)) = &elements[1].kind { assert_eq!(*n, 1); }
if let UntypedKind::Constant(Value::Int(n)) = &elements[4].kind { assert_eq!(*n, 4); }
} else { panic!("Expected Tuple result, got {:?}", result.kind); }
} else { panic!("Expected Expansion for t, got {:?}", value.kind); }
}
}
}
}
+2
View File
@@ -3,9 +3,11 @@ pub mod bound_nodes;
pub mod tco;
pub mod upvalues;
pub mod dumper;
pub mod macros;
pub use binder::*;
pub use bound_nodes::*;
pub use tco::*;
pub use upvalues::*;
pub use dumper::*;
pub use macros::*;
+6
View File
@@ -93,6 +93,12 @@ impl UpvalueAnalyzer {
Self::visit(v, scopes, capture_map, current_lambda.clone());
}
}
UntypedKind::Expansion { call: _, expanded } => {
Self::visit(expanded, scopes, capture_map, current_lambda);
}
UntypedKind::MacroDecl { body, .. } | UntypedKind::Template(body) | UntypedKind::Placeholder(body) | UntypedKind::Splice(body) => {
Self::visit(body, scopes, capture_map, current_lambda);
}
UntypedKind::Nop | UntypedKind::Constant(_) | UntypedKind::Extension(_) => {}
}
}