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
+54 -27
View File
@@ -1,7 +1,7 @@
use std::collections::{HashMap, BTreeMap};
use std::rc::Rc;
use std::cell::RefCell;
use crate::ast::nodes::{Node, UntypedKind};
use crate::ast::nodes::{Node, UntypedKind, Symbol};
use crate::ast::compiler::bound_nodes::{BoundKind, Address};
use crate::ast::types::{Identity, StaticType, Value};
@@ -13,7 +13,7 @@ struct LocalInfo {
#[derive(Debug, Clone)]
struct CompilerScope {
locals: HashMap<String, LocalInfo>,
locals: HashMap<Symbol, LocalInfo>,
slot_count: u32,
}
@@ -25,18 +25,18 @@ impl CompilerScope {
}
}
fn define(&mut self, name: &str, ty: StaticType) -> Result<u32, String> {
if self.locals.contains_key(name) {
return Err(format!("Variable '{}' is already defined in this scope level.", name));
fn define(&mut self, sym: &Symbol, ty: StaticType) -> Result<u32, String> {
if self.locals.contains_key(sym) {
return Err(format!("Variable '{}' is already defined in this scope level.", sym.name));
}
let slot = self.slot_count;
self.locals.insert(name.to_string(), LocalInfo { slot, ty });
self.locals.insert(sym.clone(), LocalInfo { slot, ty });
self.slot_count += 1;
Ok(slot)
}
fn resolve(&self, name: &str) -> Option<LocalInfo> {
self.locals.get(name).cloned()
fn resolve(&self, sym: &Symbol) -> Option<LocalInfo> {
self.locals.get(sym).cloned()
}
}
@@ -65,18 +65,18 @@ impl FunctionCompiler {
pub struct Binder {
functions: Vec<FunctionCompiler>,
// Globals mapping: Name -> (Index, Type)
globals: Rc<RefCell<HashMap<String, (u32, StaticType)>>>,
// Globals mapping: Symbol -> (Index, Type)
globals: Rc<RefCell<HashMap<Symbol, (u32, StaticType)>>>,
// Map of Declaration Identity -> List of Lambda Identities that capture it
capture_map: HashMap<Identity, Vec<Identity>>,
}
impl Binder {
pub fn new(globals: Rc<RefCell<HashMap<String, (u32, StaticType)>>>) -> Self {
pub fn new(globals: Rc<RefCell<HashMap<Symbol, (u32, StaticType)>>>) -> Self {
Self::with_boxed(globals, HashMap::new())
}
fn with_boxed(globals: Rc<RefCell<HashMap<String, (u32, StaticType)>>>, captures: HashMap<Identity, Vec<Identity>>) -> Self {
fn with_boxed(globals: Rc<RefCell<HashMap<Symbol, (u32, StaticType)>>>, captures: HashMap<Identity, Vec<Identity>>) -> Self {
let mut binder = Self {
functions: Vec::new(),
globals,
@@ -86,7 +86,7 @@ impl Binder {
binder
}
pub fn bind_root(globals: Rc<RefCell<HashMap<String, (u32, StaticType)>>>, node: &Node<UntypedKind>) -> Result<Node<BoundKind, StaticType>, String> {
pub fn bind_root(globals: Rc<RefCell<HashMap<Symbol, (u32, StaticType)>>>, node: &Node<UntypedKind>) -> Result<Node<BoundKind, StaticType>, String> {
let captures = crate::ast::compiler::upvalues::UpvalueAnalyzer::analyze(node);
let mut binder = Self::with_boxed(globals, captures);
binder.bind(node)
@@ -100,8 +100,8 @@ impl Binder {
Ok(self.make_node(node.identity.clone(), BoundKind::Constant(v.clone()), ty))
},
UntypedKind::Identifier(name) => {
let (addr, ty) = self.resolve_variable(name)?;
UntypedKind::Identifier(sym) => {
let (addr, ty) = self.resolve_variable(sym)?;
Ok(self.make_node(node.identity.clone(), BoundKind::Get(addr), ty))
},
@@ -138,11 +138,11 @@ impl Binder {
// 1. Pre-declare name to support recursion
let slot_or_idx = if self.functions.len() == 1 {
let mut globals = self.globals.borrow_mut();
if globals.contains_key(name.as_ref()) {
return Err(format!("Global variable '{}' is already defined.", name));
if globals.contains_key(name) {
return Err(format!("Global variable '{}' is already defined.", name.name));
}
let idx = globals.len() as u32;
globals.insert(name.to_string(), (idx, StaticType::Any));
globals.insert(name.clone(), (idx, StaticType::Any));
idx
} else {
let current_fn = self.functions.last_mut().unwrap();
@@ -157,7 +157,7 @@ impl Binder {
if self.functions.len() == 1 {
{
let mut globals = self.globals.borrow_mut();
if let Some(entry) = globals.get_mut(name.as_ref()) {
if let Some(entry) = globals.get_mut(name) {
entry.1 = ty.clone();
}
}
@@ -168,7 +168,7 @@ impl Binder {
} else {
{
let current_fn = self.functions.last_mut().unwrap();
if let Some(info) = current_fn.scope.locals.get_mut(name.as_ref()) {
if let Some(info) = current_fn.scope.locals.get_mut(name) {
info.ty = ty.clone();
}
}
@@ -184,8 +184,8 @@ impl Binder {
UntypedKind::Assign { target, value } => {
let val_node = self.bind(value)?;
if let UntypedKind::Identifier(name) = &target.kind {
let (addr, target_ty) = self.resolve_variable(name)?;
if let UntypedKind::Identifier(sym) = &target.kind {
let (addr, target_ty) = self.resolve_variable(sym)?;
// Type check: value must be compatible with target
if target_ty != StaticType::Any && val_node.ty != StaticType::Any && target_ty != val_node.ty {
@@ -322,17 +322,17 @@ impl Binder {
}
}
fn resolve_variable(&mut self, name: &str) -> Result<(Address, StaticType), String> {
fn resolve_variable(&mut self, sym: &Symbol) -> Result<(Address, StaticType), String> {
let current_fn_idx = self.functions.len() - 1;
// 1. Try local in current function
if let Some(info) = self.functions[current_fn_idx].scope.resolve(name) {
if let Some(info) = self.functions[current_fn_idx].scope.resolve(sym) {
return Ok((Address::Local(info.slot), info.ty));
}
// 2. Try enclosing scopes (capture chain)
for i in (0..current_fn_idx).rev() {
if let Some(info) = self.functions[i].scope.resolve(name) {
if let Some(info) = self.functions[i].scope.resolve(sym) {
let mut addr = Address::Local(info.slot);
let ty = info.ty.clone();
@@ -345,11 +345,20 @@ impl Binder {
// 3. Try Global
let globals = self.globals.borrow();
if let Some((idx, ty)) = globals.get(name) {
if let Some((idx, ty)) = globals.get(sym) {
return Ok((Address::Global(*idx), ty.clone()));
}
Err(format!("Undefined variable '{}'", name))
// 4. Global Fallback: If not found with context, try without context
// (Allows macros to access global built-ins like *, +, etc.)
if sym.context.is_some() {
let fallback_sym = Symbol { name: sym.name.clone(), context: None };
if let Some((idx, ty)) = globals.get(&fallback_sym) {
return Ok((Address::Global(*idx), ty.clone()));
}
}
Err(format!("Undefined variable '{}'", sym.name))
}
fn make_node(&self, identity: Identity, kind: BoundKind, ty: StaticType) -> Node<BoundKind, StaticType> {
@@ -426,4 +435,22 @@ mod tests {
assert!(result.is_err());
assert!(result.unwrap_err().contains("already defined"));
}
#[test]
fn test_repro_global_redefinition() {
let globals = Rc::new(RefCell::new(HashMap::new()));
// First run: defines 'x'
let source1 = "(def x 1)";
let untyped1 = Parser::new(source1).unwrap().parse_expression().unwrap();
assert!(Binder::bind_root(globals.clone(), &untyped1).is_ok());
// Second run: attempts to redefine 'x' in the same global environment
let source2 = "(def x 2)";
let untyped2 = Parser::new(source2).unwrap().parse_expression().unwrap();
let result = Binder::bind_root(globals.clone(), &untyped2);
assert!(result.is_err());
assert!(result.unwrap_err().contains("already defined"));
}
}