conlang: Variable binding and cleanup
ast: Separate concerns, and remove Walk interpreter: implement variable binding
This commit is contained in:
		| @@ -1,5 +1,6 @@ | ||||
| //! Interprets an AST as a program | ||||
|  | ||||
| use self::scope::Environment; | ||||
| use crate::ast::preamble::*; | ||||
| use error::{Error, IResult, Reason}; | ||||
| use temp_type_impl::ConValue; | ||||
| @@ -12,9 +13,10 @@ pub mod temp_type_impl { | ||||
|     /// | ||||
|     /// This is a hack to work around the fact that Conlang doesn't have a functioning type system | ||||
|     /// yet :( | ||||
|     #[derive(Clone, Debug)] | ||||
|     #[derive(Clone, Debug, Default)] | ||||
|     pub enum ConValue { | ||||
|         /// The empty/unit `()` type | ||||
|         #[default] | ||||
|         Empty, | ||||
|         /// An integer | ||||
|         Int(i128), | ||||
| @@ -57,6 +59,18 @@ pub mod temp_type_impl { | ||||
|             gt_eq: true, >=; | ||||
|             gt: false, >; | ||||
|         } | ||||
|         assign! { | ||||
|             add_assign: +; | ||||
|             bitand_assign: &; | ||||
|             bitor_assign: |; | ||||
|             bitxor_assign: ^; | ||||
|             div_assign: /; | ||||
|             mul_assign: *; | ||||
|             rem_assign: %; | ||||
|             shl_assign: <<; | ||||
|             shr_assign: >>; | ||||
|             sub_assign: -; | ||||
|         } | ||||
|     } | ||||
|     /// Templates comparison functions for [ConValue] | ||||
|     macro cmp ($($fn:ident: $empty:literal, $op:tt);*$(;)?) {$( | ||||
| @@ -73,6 +87,12 @@ pub mod temp_type_impl { | ||||
|             } | ||||
|         } | ||||
|     )*} | ||||
|     macro assign($( $fn: ident: $op: tt );*$(;)?) {$( | ||||
|         pub fn $fn(&mut self, other: Self) -> IResult<()> { | ||||
|             *self = (std::mem::take(self) $op other)?; | ||||
|             Ok(()) | ||||
|         } | ||||
|     )*} | ||||
|     /// Implements [From] for an enum with 1-tuple variants | ||||
|     macro from ($($T:ty => $v:expr),*$(,)?) { | ||||
|         $(impl From<$T> for ConValue { | ||||
| @@ -197,6 +217,7 @@ pub mod temp_type_impl { | ||||
| /// A work-in-progress tree walk interpreter for Conlang | ||||
| #[derive(Clone, Debug, Default)] | ||||
| pub struct Interpreter { | ||||
|     scope: Box<Environment>, | ||||
|     stack: Vec<ConValue>, | ||||
| } | ||||
|  | ||||
| @@ -230,6 +251,13 @@ impl Interpreter { | ||||
|     fn pop_two(&mut self) -> IResult<(ConValue, ConValue)> { | ||||
|         Ok((self.pop()?, self.pop()?)) | ||||
|     } | ||||
|     fn resolve(&mut self, value: &Identifier) -> IResult<ConValue> { | ||||
|         self.scope | ||||
|             .get(value) | ||||
|             .cloned() | ||||
|             .ok_or_else(|| Error::with_reason(Reason::NotDefined(value.to_owned())))? | ||||
|             .ok_or_else(|| Error::with_reason(Reason::NotInitialized(value.to_owned()))) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Visitor<IResult<()>> for Interpreter { | ||||
| @@ -242,10 +270,7 @@ impl Visitor<IResult<()>> for Interpreter { | ||||
|  | ||||
|     fn visit_statement(&mut self, stmt: &Stmt) -> IResult<()> { | ||||
|         match stmt { | ||||
|             Stmt::Let { name, mutable, ty, init } => todo!( | ||||
|                 "let{} {name:?}: {ty:?} = {init:?}", | ||||
|                 if *mutable { " mut" } else { "" } | ||||
|             ), | ||||
|             Stmt::Let(l) => self.visit_let(l), | ||||
|             Stmt::Expr(e) => { | ||||
|                 self.visit_expr(e)?; | ||||
|                 self.pop().map(drop) | ||||
| @@ -253,57 +278,124 @@ impl Visitor<IResult<()>> for Interpreter { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn visit_operation(&mut self, expr: &math::Operation) -> IResult<()> { | ||||
|         use math::Operation; | ||||
|         // TODO: the indentation depth here is driving me insane. | ||||
|         //       maybe refactor the ast to break binary and unary | ||||
|         //       operations into their own nodes, and use | ||||
|         //       Operation to unify them? | ||||
|         match expr { | ||||
|             Operation::Binary { first, other } => { | ||||
|                 self.visit_operation(first)?; | ||||
|                 for (op, other) in other { | ||||
|                     match op { | ||||
|                         operator::Binary::LogAnd => { | ||||
|                             if self.peek()?.truthy()? { | ||||
|                                 self.pop()?; | ||||
|                                 self.visit_operation(other)?; | ||||
|                             } | ||||
|                         } | ||||
|                         operator::Binary::LogOr => { | ||||
|                             if !self.peek()?.truthy()? { | ||||
|                                 self.pop()?; | ||||
|                                 self.visit_operation(other)?; | ||||
|                             } | ||||
|                         } | ||||
|                         operator::Binary::LogXor => { | ||||
|                             let first = self.pop()?.truthy()?; | ||||
|                             self.visit_operation(other)?; | ||||
|                             let second = self.pop()?.truthy()?; | ||||
|                             self.push(first ^ second); | ||||
|                         } | ||||
|                         _ => { | ||||
|                             self.visit_operation(other)?; | ||||
|                             self.visit_binary_op(op)?; | ||||
|                         } | ||||
|     fn visit_let(&mut self, stmt: &Let) -> IResult<()> { | ||||
|         let Let { name, init, .. } = stmt; | ||||
|         if let Some(init) = init { | ||||
|             self.visit_expr(init)?; | ||||
|             let init = self.pop()?; | ||||
|             self.scope.insert(name, Some(init)); | ||||
|         } else { | ||||
|             self.scope.insert(name, None); | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn visit_block(&mut self, block: &expression::Block) -> IResult<()> { | ||||
|         for stmt in &block.statements { | ||||
|             self.visit_statement(stmt)?; | ||||
|         } | ||||
|         if let Some(expr) = block.expr.as_ref() { | ||||
|             self.visit_expr(expr) | ||||
|         } else { | ||||
|             self.push(ConValue::Empty); | ||||
|             Ok(()) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn visit_assign(&mut self, assign: &math::Assign) -> IResult<()> { | ||||
|         use operator::Assign; | ||||
|         let math::Assign { target, operator, init } = assign; | ||||
|         self.visit_operation(init)?; | ||||
|         let init = self.pop()?; | ||||
|         let Some(resolved) = self.scope.get_mut(target) else { | ||||
|             Err(Error::with_reason(Reason::NotDefined(target.to_owned())))? | ||||
|         }; | ||||
|         if let Assign::Assign = operator { | ||||
|             use std::mem::discriminant as variant; | ||||
|             // runtime typecheck | ||||
|             match resolved.as_mut() { | ||||
|                 Some(value) if variant(value) == variant(&init) => { | ||||
|                     *value = init; | ||||
|                 } | ||||
|                 None => *resolved = Some(init), | ||||
|                 _ => Err(Error::with_reason(Reason::TypeError))?, | ||||
|             } | ||||
|             self.push(ConValue::Empty); | ||||
|             return Ok(()); | ||||
|         } | ||||
|         let Some(target) = resolved.as_mut() else { | ||||
|             Err(Error::with_reason(Reason::NotInitialized( | ||||
|                 target.to_owned(), | ||||
|             )))? | ||||
|         }; | ||||
|         match operator { | ||||
|             Assign::AddAssign => target.add_assign(init)?, | ||||
|             Assign::SubAssign => target.sub_assign(init)?, | ||||
|             Assign::MulAssign => target.mul_assign(init)?, | ||||
|             Assign::DivAssign => target.div_assign(init)?, | ||||
|             Assign::RemAssign => target.rem_assign(init)?, | ||||
|             Assign::BitAndAssign => target.bitand_assign(init)?, | ||||
|             Assign::BitOrAssign => target.bitor_assign(init)?, | ||||
|             Assign::BitXorAssign => target.bitxor_assign(init)?, | ||||
|             Assign::ShlAssign => target.shl_assign(init)?, | ||||
|             Assign::ShrAssign => target.shr_assign(init)?, | ||||
|             _ => (), | ||||
|         } | ||||
|         self.push(ConValue::Empty); | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn visit_binary(&mut self, bin: &math::Binary) -> IResult<()> { | ||||
|         use math::Binary; | ||||
|         let Binary { first, other } = bin; | ||||
|  | ||||
|         self.visit_operation(first)?; | ||||
|         for (op, other) in other { | ||||
|             match op { | ||||
|                 operator::Binary::LogAnd => { | ||||
|                     if self.peek()?.truthy()? { | ||||
|                         self.pop()?; | ||||
|                         self.visit_operation(other)?; | ||||
|                     } | ||||
|                 } | ||||
|                 Ok(()) | ||||
|             } | ||||
|             Operation::Unary { operators, operand } => { | ||||
|                 self.visit_primary(operand)?; | ||||
|                 for op in operators.iter().rev() { | ||||
|                     self.visit_unary_op(op)?; | ||||
|                 operator::Binary::LogOr => { | ||||
|                     if !self.peek()?.truthy()? { | ||||
|                         self.pop()?; | ||||
|                         self.visit_operation(other)?; | ||||
|                     } | ||||
|                 } | ||||
|                 operator::Binary::LogXor => { | ||||
|                     let first = self.pop()?.truthy()?; | ||||
|                     self.visit_operation(other)?; | ||||
|                     let second = self.pop()?.truthy()?; | ||||
|                     self.push(first ^ second); | ||||
|                 } | ||||
|                 _ => { | ||||
|                     self.visit_operation(other)?; | ||||
|                     self.visit_binary_op(op)?; | ||||
|                 } | ||||
|                 Ok(()) | ||||
|             } | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn visit_unary(&mut self, unary: &math::Unary) -> IResult<()> { | ||||
|         let math::Unary { operand, operators } = unary; | ||||
|         self.visit_operation(operand)?; | ||||
|         for op in operators.iter().rev() { | ||||
|             self.visit_unary_op(op)?; | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn visit_assign_op(&mut self, _: &operator::Assign) -> IResult<()> { | ||||
|         unimplemented!("visit_assign_op is implemented in visit_operation") | ||||
|     } | ||||
|  | ||||
|     fn visit_binary_op(&mut self, op: &operator::Binary) -> IResult<()> { | ||||
|         use operator::Binary; | ||||
|         let (second, first) = self.pop_two()?; | ||||
|         self.push(match op { | ||||
|         let out = match op { | ||||
|             Binary::Mul => first * second, | ||||
|             Binary::Div => first / second, | ||||
|             Binary::Rem => first % second, | ||||
| @@ -325,18 +417,8 @@ impl Visitor<IResult<()>> for Interpreter { | ||||
|             Binary::NotEq => first.neq(&second), | ||||
|             Binary::GreaterEq => first.gt_eq(&second), | ||||
|             Binary::Greater => first.gt(&second), | ||||
|             Binary::Assign => todo!("Assignment"), | ||||
|             Binary::AddAssign => todo!("Assignment"), | ||||
|             Binary::SubAssign => todo!("Assignment"), | ||||
|             Binary::MulAssign => todo!("Assignment"), | ||||
|             Binary::DivAssign => todo!("Assignment"), | ||||
|             Binary::RemAssign => todo!("Assignment"), | ||||
|             Binary::BitAndAssign => todo!("Assignment"), | ||||
|             Binary::BitOrAssign => todo!("Assignment"), | ||||
|             Binary::BitXorAssign => todo!("Assignment"), | ||||
|             Binary::ShlAssign => todo!("Assignment"), | ||||
|             Binary::ShrAssign => todo!("Assignment"), | ||||
|         }?); | ||||
|         }?; | ||||
|         self.push(out); | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
| @@ -364,12 +446,13 @@ impl Visitor<IResult<()>> for Interpreter { | ||||
|             self.visit_block(&expr.body)?; | ||||
|         } else if let Some(block) = &expr.else_ { | ||||
|             self.visit_else(block)?; | ||||
|         } else { | ||||
|             self.push(ConValue::Empty) | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn visit_while(&mut self, expr: &control::While) -> IResult<()> { | ||||
|         let mut broke = false; | ||||
|         while { | ||||
|             self.visit_expr(&expr.cond)?; | ||||
|             self.pop()?.truthy()? | ||||
| @@ -384,26 +467,28 @@ impl Visitor<IResult<()>> for Interpreter { | ||||
|                 Reason::Continue => continue, | ||||
|                 Reason::Break(value) => { | ||||
|                     self.push(value); | ||||
|                     broke = true; | ||||
|                     break; | ||||
|                     return Ok(()); | ||||
|                 } | ||||
|                 r => Err(Error::with_reason(r))?, | ||||
|             } | ||||
|         } | ||||
|         if let (Some(r#else), false) = (&expr.else_, broke) { | ||||
|         if let Some(r#else) = &expr.else_ { | ||||
|             self.visit_else(r#else)?; | ||||
|         } else { | ||||
|             self.push(ConValue::Empty); | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn visit_for(&mut self, expr: &control::For) -> IResult<()> { | ||||
|         self.scope.enter(); | ||||
|         self.visit_expr(&expr.iter)?; | ||||
|         let mut broke = false; | ||||
|         let bounds = match self.pop()? { | ||||
|             ConValue::RangeExc(a, b) | ConValue::RangeInc(a, b) => (a, b), | ||||
|             _ => Err(Error::with_reason(Reason::NotIterable))?, | ||||
|         }; | ||||
|         for _ in bounds.0..=bounds.1 { | ||||
|         for loop_var in bounds.0..=bounds.1 { | ||||
|             self.scope.insert(&expr.var, Some(loop_var.into())); | ||||
|             let Err(out) = self.visit_block(&expr.body) else { | ||||
|                 self.pop()?; | ||||
|                 continue; | ||||
| @@ -412,15 +497,17 @@ impl Visitor<IResult<()>> for Interpreter { | ||||
|                 Reason::Continue => continue, | ||||
|                 Reason::Break(value) => { | ||||
|                     self.push(value); | ||||
|                     broke = true; | ||||
|                     break; | ||||
|                     return Ok(()); | ||||
|                 } | ||||
|                 r => Err(Error::with_reason(r))?, | ||||
|             } | ||||
|         } | ||||
|         if let (Some(r#else), false) = (&expr.else_, broke) { | ||||
|         if let Some(r#else) = &expr.else_ { | ||||
|             self.visit_else(r#else)?; | ||||
|         } else { | ||||
|             self.push(ConValue::Empty) | ||||
|         } | ||||
|         self.scope.exit()?; | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
| @@ -447,7 +534,9 @@ impl Visitor<IResult<()>> for Interpreter { | ||||
|     } | ||||
|  | ||||
|     fn visit_identifier(&mut self, ident: &Identifier) -> IResult<()> { | ||||
|         todo!("Identifier lookup and scoping rules: {ident:?}") | ||||
|         let value = self.resolve(ident)?; | ||||
|         self.push(value); | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn visit_string_literal(&mut self, string: &str) -> IResult<()> { | ||||
| @@ -480,8 +569,69 @@ impl Visitor<IResult<()>> for Interpreter { | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub mod scope { | ||||
|     //! Lexical and non-lexical scoping for variables | ||||
|     use super::{ | ||||
|         error::{Error, IResult, Reason}, | ||||
|         temp_type_impl::ConValue, | ||||
|         Identifier, | ||||
|     }; | ||||
|     use std::collections::HashMap; | ||||
|  | ||||
|     #[derive(Clone, Debug, Default)] | ||||
|     pub enum Variable { | ||||
|         #[default] | ||||
|         Uninit, | ||||
|         Init(ConValue), | ||||
|     } | ||||
|  | ||||
|     /// Implements a nested lexical scope | ||||
|     #[derive(Clone, Debug, Default)] | ||||
|     pub struct Environment { | ||||
|         outer: Option<Box<Self>>, | ||||
|         vars: HashMap<Identifier, Option<ConValue>>, | ||||
|     } | ||||
|  | ||||
|     impl Environment { | ||||
|         /// Enter a nested scope | ||||
|         pub fn enter(self: &mut Box<Self>) { | ||||
|             let outer = std::mem::take(self); | ||||
|             self.outer = Some(outer); | ||||
|         } | ||||
|         /// Exits the scope, destroying all local variables and | ||||
|         /// returning the outer scope, if there is one | ||||
|         pub fn exit(&mut self) -> IResult<()> { | ||||
|             if let Some(outer) = std::mem::take(&mut self.outer) { | ||||
|                 *self = *outer; | ||||
|                 Ok(()) | ||||
|             } else { | ||||
|                 Err(Error::with_reason(Reason::ScopeExit)) | ||||
|             } | ||||
|         } | ||||
|         /// Resolves a variable mutably | ||||
|         pub fn get_mut(&mut self, id: &Identifier) -> Option<&mut Option<ConValue>> { | ||||
|             match self.vars.get_mut(id) { | ||||
|                 Some(var) => Some(var), | ||||
|                 None => self.outer.as_mut().and_then(|o| o.get_mut(id)), | ||||
|             } | ||||
|         } | ||||
|         /// Resolves a variable immutably | ||||
|         pub fn get(&self, id: &Identifier) -> Option<&Option<ConValue>> { | ||||
|             match self.vars.get(id) { | ||||
|                 Some(var) => Some(var), | ||||
|                 None => self.outer.as_ref().and_then(|o| o.get(id)), | ||||
|             } | ||||
|         } | ||||
|         pub fn insert(&mut self, id: &Identifier, value: Option<ConValue>) { | ||||
|             self.vars.insert(id.clone(), value); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub mod error { | ||||
|     //! The [Error] type represents any error thrown by the [Interpreter](super::Interpreter) | ||||
|     use crate::ast::Identifier; | ||||
|  | ||||
|     use super::temp_type_impl::ConValue; | ||||
|  | ||||
|     pub type IResult<T> = Result<T, Error>; | ||||
| @@ -524,11 +674,17 @@ pub mod error { | ||||
|         Continue, | ||||
|         /// Underflowed the stack | ||||
|         StackUnderflow, | ||||
|         /// Exited the last scope | ||||
|         ScopeExit, | ||||
|         /// Type incompatibility | ||||
|         // TODO: store the type information in this error | ||||
|         TypeError, | ||||
|         /// In clause of For loop didn't yield a Range | ||||
|         NotIterable, | ||||
|         /// A name was not defined in scope before being used | ||||
|         NotDefined(Identifier), | ||||
|         /// A name was defined but not initialized | ||||
|         NotInitialized(Identifier), | ||||
|     } | ||||
|  | ||||
|     impl std::error::Error for Error {} | ||||
| @@ -540,12 +696,19 @@ pub mod error { | ||||
|     impl std::fmt::Display for Reason { | ||||
|         fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|             match self { | ||||
|                 Reason::Return(value) => write!(f, "return {value:?}"), | ||||
|                 Reason::Break(value) => write!(f, "break {value:?}"), | ||||
|                 Reason::Return(value) => write!(f, "return {value}"), | ||||
|                 Reason::Break(value) => write!(f, "break {value}"), | ||||
|                 Reason::Continue => "continue".fmt(f), | ||||
|                 Reason::StackUnderflow => "Stack underflow".fmt(f), | ||||
|                 Reason::TypeError => "Type error".fmt(f), | ||||
|                 Reason::ScopeExit => "Exited the last scope. This is a logic bug.".fmt(f), | ||||
|                 Reason::TypeError => "Incompatible types".fmt(f), | ||||
|                 Reason::NotIterable => "`in` clause of `for` loop did not yield an iterable".fmt(f), | ||||
|                 Reason::NotDefined(value) => { | ||||
|                     write!(f, "{} not bound. Did you mean `let {};`?", value.0, value.0) | ||||
|                 } | ||||
|                 Reason::NotInitialized(value) => { | ||||
|                     write!(f, "{} bound, but not initialized", value.0) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user