Refactor: Use Symbol for identifiers and macro hygiene

Introduces a `Symbol` struct to represent identifiers, incorporating
macro hygiene context. Updates various parts of the AST compiler and
environment to use `Symbol` instead of raw `Rc<str>` for identifiers,
improving robustness for macro expansions.

Also includes:
- Adds a new example `macro_hygiene.myc`.
- Updates `.gitignore`.
- Refactors `MacroExpander` to use `ExpansionState` for cleaner template
  expansion.
- Adjusts `VM::eval_observed` to ensure `after_eval` is called
  correctly.
- Resets `Environment` for each test run and compilation/dumping in
  `main.rs`.
This commit is contained in:
Michael Schimmel
2026-02-18 14:32:09 +01:00
parent 73ddd644c1
commit 76586c0903
12 changed files with 363 additions and 123 deletions
+192 -49
View File
@@ -1,17 +1,31 @@
use std::collections::HashMap;
use std::rc::Rc;
use crate::ast::nodes::{Node, UntypedKind};
use crate::ast::nodes::{Node, UntypedKind, Symbol};
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>;
}
/// Internal state for template expansion (Hygiene context + Parameter binding)
struct ExpansionState<'a> {
bindings: &'a HashMap<Rc<str>, Node<UntypedKind>>,
/// The identity of the current expansion instance, used to "color" internal symbols.
expansion_id: Identity,
}
type MacroMap = HashMap<Rc<str>, (Vec<Rc<str>>, Node<UntypedKind>)>;
/// A registry for macro declarations.
pub struct MacroRegistry {
scopes: Vec<HashMap<Rc<str>, (Vec<Rc<str>>, Node<UntypedKind>)>>,
scopes: Vec<MacroMap>,
}
impl Default for MacroRegistry {
fn default() -> Self {
Self::new()
}
}
impl MacroRegistry {
@@ -64,7 +78,8 @@ impl<E: MacroEvaluator> MacroExpander<E> {
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);
let p_names: Vec<Rc<str>> = params.into_iter().map(|s| s.name).collect();
self.registry.define(name.name, p_names, *body);
Ok(Node {
identity: node.identity,
kind: UntypedKind::Nop,
@@ -73,15 +88,14 @@ impl<E: MacroEvaluator> MacroExpander<E> {
}
UntypedKind::Call { callee, args } => {
if let UntypedKind::Identifier(ref name) = callee.kind {
if let Some((params, body)) = self.registry.lookup(name) {
if let UntypedKind::Identifier(ref sym) = callee.kind
&& let Some((params, body)) = self.registry.lookup(&sym.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)?;
let expanded = self.expand_call(node.identity.clone(), &sym.name, params, expanded_args, body)?;
return self.expand_recursive(expanded);
}
}
let expanded_callee = self.expand_recursive(*callee)?;
@@ -208,9 +222,15 @@ impl<E: MacroEvaluator> MacroExpander<E> {
bindings.insert(p, a);
}
// AST-Authority: Substitution and Hygiene ONLY happen if there is a Template node.
let expanded_body = if let UntypedKind::Template(inner) = body.kind {
self.expand_template(*inner, &bindings)?
let mut state = ExpansionState {
bindings: &bindings,
expansion_id: identity.clone(),
};
self.expand_template(*inner, &mut state)?
} else {
// Static AST fragment: No substitution, no hygiene.
body
};
@@ -219,10 +239,10 @@ impl<E: MacroEvaluator> MacroExpander<E> {
kind: UntypedKind::Call {
callee: Box::new(Node {
identity: identity.clone(),
kind: UntypedKind::Identifier(Rc::from(name)),
kind: UntypedKind::Identifier(Symbol::from(name)),
ty: (),
}),
args: bindings.iter().map(|(_, v)| v.clone()).collect(),
args: bindings.into_values().collect(),
},
ty: (),
};
@@ -237,25 +257,62 @@ impl<E: MacroEvaluator> MacroExpander<E> {
})
}
fn expand_template(&self, node: Node<UntypedKind>, bindings: &HashMap<Rc<str>, Node<UntypedKind>>) -> Result<Node<UntypedKind>, String> {
fn expand_template(&self, node: Node<UntypedKind>, state: &mut ExpansionState) -> Result<Node<UntypedKind>, String> {
match node.kind {
UntypedKind::Identifier(mut sym) => {
// Inside a template, all internal identifiers are colored.
sym.context = Some(state.expansion_id.clone());
Ok(Node { identity: node.identity, kind: UntypedKind::Identifier(sym), ty: () })
}
UntypedKind::Placeholder(inner) => {
let val = self.evaluator.evaluate(&inner, bindings)?;
// Break out of template for substitution/evaluation
if let UntypedKind::Identifier(ref sym) = inner.kind
&& let Some(arg) = state.bindings.get(&sym.name) {
return Ok(arg.clone());
}
let val = self.evaluator.evaluate(&inner, state.bindings)?;
Ok(self.value_to_node(val, node.identity))
}
UntypedKind::Splice(_) => {
Err("Splice node found outside of a container context".to_string())
UntypedKind::Splice(inner) => {
// Splicing is also a form of substitution
if let UntypedKind::Identifier(ref sym) = inner.kind
&& let Some(arg) = state.bindings.get(&sym.name) {
return Ok(arg.clone());
}
let val = self.evaluator.evaluate(&inner, state.bindings)?;
Ok(self.value_to_node(val, node.identity))
}
UntypedKind::Def { mut name, value } => {
name.context = Some(state.expansion_id.clone());
let val = self.expand_template(*value, state)?;
Ok(Node {
identity: node.identity,
kind: UntypedKind::Def { name, value: Box::new(val) },
ty: (),
})
}
UntypedKind::Assign { target, value } => {
let target = self.expand_template(*target, state)?;
let value = self.expand_template(*value, state)?;
Ok(Node {
identity: node.identity,
kind: UntypedKind::Assign { target: Box::new(target), value: Box::new(value) },
ty: (),
})
}
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)?;
let val = self.evaluator.evaluate(inner, state.bindings)?;
self.handle_splice_value(val, node.identity.clone(), &mut new_elements)?;
} else {
new_elements.push(self.expand_template(e, bindings)?);
new_elements.push(self.expand_template(e, state)?);
}
}
Ok(Node {
@@ -269,10 +326,10 @@ impl<E: MacroEvaluator> MacroExpander<E> {
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)?;
let val = self.evaluator.evaluate(inner, state.bindings)?;
self.handle_splice_value(val, node.identity.clone(), &mut new_exprs)?;
} else {
new_exprs.push(self.expand_template(e, bindings)?);
new_exprs.push(self.expand_template(e, state)?);
}
}
Ok(Node {
@@ -285,7 +342,7 @@ impl<E: MacroEvaluator> MacroExpander<E> {
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)?));
new_entries.push((self.expand_template(k, state)?, self.expand_template(v, state)?));
}
Ok(Node {
identity: node.identity,
@@ -295,14 +352,14 @@ impl<E: MacroEvaluator> MacroExpander<E> {
}
UntypedKind::Call { callee, args } => {
let expanded_callee = self.expand_template(*callee, bindings)?;
let expanded_callee = self.expand_template(*callee, state)?;
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)?;
let val = self.evaluator.evaluate(inner, state.bindings)?;
self.handle_splice_value(val, node.identity.clone(), &mut expanded_args)?;
} else {
expanded_args.push(self.expand_template(arg, bindings)?);
expanded_args.push(self.expand_template(arg, state)?);
}
}
Ok(Node {
@@ -313,10 +370,10 @@ impl<E: MacroEvaluator> MacroExpander<E> {
}
UntypedKind::If { cond, then_br, else_br } => {
let cond = self.expand_template(*cond, bindings)?;
let then_br = self.expand_template(*then_br, bindings)?;
let cond = self.expand_template(*cond, state)?;
let then_br = self.expand_template(*then_br, state)?;
let else_br = if let Some(e) = else_br {
Some(Box::new(self.expand_template(*e, bindings)?))
Some(Box::new(self.expand_template(*e, state)?))
} else {
None
};
@@ -327,6 +384,18 @@ impl<E: MacroEvaluator> MacroExpander<E> {
})
}
UntypedKind::Lambda { mut params, body } => {
for p in &mut params {
p.context = Some(state.expansion_id.clone());
}
let body = self.expand_template(Node::clone(&body), state)?;
Ok(Node {
identity: node.identity,
kind: UntypedKind::Lambda { params, body: Rc::new(body) },
ty: (),
})
}
_ => Ok(node),
}
}
@@ -369,28 +438,51 @@ impl<E: MacroEvaluator> MacroExpander<E> {
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
use crate::ast::parser::Parser;
use crate::ast::types::Value;
use crate::ast::types::{Value, StaticType, Object};
use crate::ast::compiler::Binder;
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())));
}
if let UntypedKind::Identifier(ref sym) = node.kind
&& let Some(arg) = bindings.get(&sym.name) {
return Ok(Value::Object(Rc::new(arg.clone()) as Rc<dyn Object>));
}
// For real expressions, we would need Binder + VM here.
Err(format!("SimpleEvaluator cannot evaluate complex expression: {:?}", node.kind))
}
}
#[test]
fn test_macro_expansion_unless() {
fn test_macro_expansion_hygiene() {
let source = "
(do
(macro unless [c b] `(if ~c void ~b))
(unless true 42))
(macro m [] `(def y 10))
(m))
";
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::Expansion { expanded: result, call } = &exprs[1].kind {
if let UntypedKind::Def { name, .. } = &result.kind {
assert_eq!(name.context, Some(call.identity.clone()));
assert_eq!(name.name.as_ref(), "y");
} else { panic!("Expected Def result, got {:?}", result.kind); }
}
}
}
#[test]
fn test_macro_expansion_unless() {
let source = "
(do
(macro unless [c b] `(if ~c void ~b))
(unless false 42))
";
let mut parser = Parser::new(source).unwrap();
let untyped = parser.parse_expression().unwrap();
@@ -399,28 +491,19 @@ mod tests {
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_eq!(*b, false);
} else { panic!("Expected false, 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");
}
}
@@ -443,11 +526,71 @@ mod tests {
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); }
}
}
}
}
}
#[test]
fn test_repro_macro_global_lookup() {
let source = "
(do
(macro square [x] `(* ~x ~x))
(square 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();
let mut global_names = HashMap::new();
global_names.insert(Symbol::from("*"), (0, StaticType::Any));
let globals = Rc::new(RefCell::new(global_names));
let result = Binder::bind_root(globals, &expanded);
assert!(result.is_ok(), "Should find global '*' Error: {:?}", result.err());
}
#[test]
fn test_repro_macro_hygiene_success() {
let source = "
(do
(def y 1)
(macro m [] `(def y 10))
(m)
y)
";
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();
let globals = Rc::new(RefCell::new(HashMap::new()));
let bound = Binder::bind_root(globals, &expanded);
assert!(bound.is_ok(), "Hygiene failed: {:?}", bound.err());
}
#[test]
fn test_repro_macro_hygiene_clash_fixed() {
let source = "
(do
(def y 1)
(macro m1 [x] `(do (def y 10) (assign y 4)))
(m1 8)
y)
";
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();
let globals = Rc::new(RefCell::new(HashMap::new()));
let bound = Binder::bind_root(globals, &expanded);
assert!(bound.is_ok(), "Explicit Hygiene with Backticks failed: {:?}", bound.err());
}
}