diff --git a/src/error.rs b/src/error.rs index 3d3b94b3..c994bbd0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -14,4 +14,5 @@ pub enum ErrorKind { UnknownVariable(String), MeloVariable(String), TypeError(String), + TopLevelBreak, } diff --git a/src/interpret.rs b/src/interpret.rs index 051b0923..9fe6371a 100644 --- a/src/interpret.rs +++ b/src/interpret.rs @@ -1,10 +1,10 @@ //! Expression evaluator and statement interpreter. //! //! To interpret a piece of AbleScript code, you first need to -//! construct a [Scope], which is responsible for storing the list of -//! variable and function definitions accessible from an AbleScript -//! snippet. You can then call [Scope::eval_items] to evaluate or -//! execute any number of expressions or statements. +//! construct an [ExecEnv], which is responsible for storing the stack +//! of local variable and function definitions accessible from an +//! AbleScript snippet. You can then call [ExecEnv::eval_items] to +//! evaluate or execute any number of expressions or statements. #[deny(missing_docs)] use std::collections::HashMap; @@ -16,41 +16,91 @@ use crate::{ variables::{Value, Variable}, }; -/// A set of visible variable and function definitions, which serves -/// as a context in which expressions can be evaluated. -pub struct Scope { - /// The mapping from variable names to values. - variables: HashMap, - - // In the future, this will store functio definitions, a link to a - // parent scope (so we can have nested scopes), and possibly other - // information. +/// An environment for executing AbleScript code. +pub struct ExecEnv { + /// The stack, ordered such that `stack[stack.len() - 1]` is the + /// top-most (newest) stack frame, and `stack[0]` is the + /// bottom-most (oldest) stack frame. + stack: Vec, } -impl Scope { +/// A set of visible variable and function definitions in a single +/// stack frame. +#[derive(Default)] +struct Scope { + /// The mapping from variable names to values. + variables: HashMap, + // In the future, this will store functio definitions and possibly + // other information. +} + +/// The reason a successful series of statements halted. +enum HaltStatus { + /// The last statement in the list evaluated to this value. + Value(Value), + + /// A `break` statement occurred and was not caught by a `loop` + /// statement. + Break, + + /// A `hopback` statement occurred and was not caught by a `loop` + /// statement. + Hopback, +} + +impl ExecEnv { /// Create a new Scope with no predefined variable definitions or /// other information. pub fn new() -> Self { Self { - variables: HashMap::new(), + stack: Default::default(), } } - /// Evaluate a set of Items. Returns the value of the last Item - /// evaluated, or an error if one or more of the Items failed to - /// evaluate. + /// Evaluate a set of Items in their own stack frame. Return the + /// value of the last Item evaluated, or an error if one or more + /// of the Items failed to evaluate or if a `break` or `hopback` + /// statement occurred at the top level. pub fn eval_items(&mut self, items: &[Item]) -> Result { - items - .iter() - .map(|item| self.eval_item(item)) - .try_fold(Value::Nul, |_, result| result) + match self.eval_items_hs(items)? { + HaltStatus::Value(v) => Ok(v), + HaltStatus::Break | HaltStatus::Hopback => Err(Error { + // It's an error to issue a `break` outside of a + // `loop` statement. + kind: ErrorKind::TopLevelBreak, + position: 0..0, + }), + } + } + + /// The same as `eval_items`, but report "break" and "hopback" + /// exit codes as normal conditions in a HaltStatus enum. + /// + /// `interpret`-internal code should typically prefer this + /// function over `eval_items`. + fn eval_items_hs(&mut self, items: &[Item]) -> Result { + let init_depth = self.stack.len(); + + self.stack.push(Default::default()); + let mut final_result = Ok(HaltStatus::Value(Value::Nul)); + for item in items { + final_result = self.eval_item(item); + if !matches!(final_result, Ok(HaltStatus::Value(_))) { + break; + } + } + self.stack.pop(); + + // Invariant: stack size must have net 0 change. + debug_assert_eq!(self.stack.len(), init_depth); + final_result } /// Evaluate a single Item, returning its value or an error. - fn eval_item(&mut self, item: &Item) -> Result { + fn eval_item(&mut self, item: &Item) -> Result { match item { - Item::Expr(expr) => self.eval_expr(expr), - Item::Stmt(stmt) => self.eval_stmt(stmt).map(|_| Value::Nul), + Item::Expr(expr) => self.eval_expr(expr).map(|v| HaltStatus::Value(v)), + Item::Stmt(stmt) => self.eval_stmt(stmt), } } @@ -82,46 +132,131 @@ impl Scope { } Eq { left, right } => Bool(self.eval_expr(left)? == self.eval_expr(right)?), Neq { left, right } => Bool(self.eval_expr(left)? != self.eval_expr(right)?), - And { left, right } => Bool( - bool::try_from(self.eval_expr(left)?)? && bool::try_from(self.eval_expr(right)?)?, - ), - Or { left, right } => Bool( - bool::try_from(self.eval_expr(left)?)? || bool::try_from(self.eval_expr(right)?)?, - ), - Not(expr) => Bool(!bool::try_from(self.eval_expr(expr)?)?), + And { left, right } => { + Bool(bool::from(self.eval_expr(left)?) && bool::from(self.eval_expr(right)?)) + } + Or { left, right } => { + Bool(bool::from(self.eval_expr(left)?) || bool::from(self.eval_expr(right)?)) + } + Not(expr) => Bool(!bool::from(self.eval_expr(expr)?)), Literal(value) => value.clone(), - Identifier(Iden(name)) => self - .variables - .get(name) - .ok_or_else(|| Error { - kind: ErrorKind::UnknownVariable(name.to_owned()), - // TODO: figure out some way to avoid this 0..0 - // dumbness - position: 0..0, - }) - .and_then(|var| { - if !var.melo { - Ok(var.value.clone()) - } else { - Err(Error { - kind: ErrorKind::MeloVariable(name.to_owned()), - position: 0..0, - }) - } - })?, + Identifier(Iden(name)) => self.get_var(name)?.value.clone(), }) } /// Perform the action indicated by a statement. - fn eval_stmt(&mut self, stmt: &Stmt) -> Result<(), Error> { + fn eval_stmt(&mut self, stmt: &Stmt) -> Result { match stmt { Stmt::Print(expr) => { println!("{}", self.eval_expr(expr)?); - Ok(()) } - _ => { - todo!() + Stmt::VariableDeclaration { iden, init } => { + let init = match init { + Some(e) => self.eval_expr(e)?, + None => Value::Nul, + }; + + // There's always at least one stack frame on the + // stack if we're evaluating something, so we can + // `unwrap` here. + self.stack.iter_mut().last().unwrap().variables.insert( + iden.0.clone(), + Variable { + melo: false, + value: init, + }, + ); } + Stmt::FunctionDeclaration { + iden: _, + args: _, + body: _, + } => todo!(), + Stmt::BfFDeclaration { iden: _, body: _ } => todo!(), + Stmt::If { cond, body } => { + if self.eval_expr(cond)?.into() { + return self.eval_items_hs(body); + } + } + Stmt::FunctionCall { iden: _, args: _ } => todo!(), + Stmt::Loop { body } => loop { + let res = self.eval_items_hs(body)?; + match res { + HaltStatus::Value(_) => {} + HaltStatus::Break => break, + HaltStatus::Hopback => continue, + } + }, + Stmt::VarAssignment { iden, value } => { + self.get_var_mut(&iden.0)?.value = self.eval_expr(value)?; + } + Stmt::Break => { + return Ok(HaltStatus::Break); + } + Stmt::HopBack => { + return Ok(HaltStatus::Hopback); + } + Stmt::Melo(iden) => { + self.get_var_mut(&iden.0)?.melo = true; + } + } + + Ok(HaltStatus::Value(Value::Nul)) + } + + /// Get a shared reference to the value of a variable. Throw an + /// error if the variable is inaccessible or banned. + fn get_var(&self, name: &str) -> Result<&Variable, Error> { + match self + .stack + .iter() + .rev() + .find_map(|scope| scope.variables.get(name)) + { + Some(var) => { + if !var.melo { + Ok(var) + } else { + Err(Error { + kind: ErrorKind::MeloVariable(name.to_owned()), + // TODO: figure out some way to avoid this + // 0..0 dumbness + position: 0..0, + }) + } + } + None => Err(Error { + kind: ErrorKind::UnknownVariable(name.to_owned()), + position: 0..0, + }), + } + } + + /// Get a mutable reference to a variable. Throw an error if the + /// variable is inaccessible or banned. + fn get_var_mut(&mut self, name: &str) -> Result<&mut Variable, Error> { + // This function is almost exactly 22 lines of duplicated code + // from get_var, which I feel like is a bad sign... + match self + .stack + .iter_mut() + .rev() + .find_map(|scope| scope.variables.get_mut(name)) + { + Some(var) => { + if !var.melo { + Ok(var) + } else { + Err(Error { + kind: ErrorKind::MeloVariable(name.to_owned()), + position: 0..0, + }) + } + } + None => Err(Error { + kind: ErrorKind::UnknownVariable(name.to_owned()), + position: 0..0, + }), } } } diff --git a/src/main.rs b/src/main.rs index 94626c8b..2cebb37b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ mod repl; mod variables; use clap::{App, Arg}; -use interpret::Scope; +use interpret::ExecEnv; use logos::Source; use parser::Parser; @@ -41,8 +41,8 @@ fn main() { match ast { Ok(ast) => { println!("{:#?}", ast); - let mut ctx = Scope::new(); - println!("{:?}", ctx.eval_items(&ast)); + let mut env = ExecEnv::new(); + println!("{:?}", env.eval_items(&ast)); } Err(e) => { println!( diff --git a/src/repl.rs b/src/repl.rs index 58ac3987..a442a974 100644 --- a/src/repl.rs +++ b/src/repl.rs @@ -1,11 +1,11 @@ use logos::Source; use rustyline::Editor; -use crate::{interpret::Scope, parser::Parser}; +use crate::{interpret::ExecEnv, parser::Parser}; pub fn repl() { let mut rl = Editor::<()>::new(); - let mut ctx = Scope::new(); + let mut env = ExecEnv::new(); loop { let readline = rl.readline(":: "); match readline { @@ -19,7 +19,7 @@ pub fn repl() { match ast { Ok(ast) => { println!("{:?}", ast); - println!("{:?}", ctx.eval_items(&ast)); + println!("{:?}", env.eval_items(&ast)); }, Err(e) => { println!( diff --git a/src/variables.rs b/src/variables.rs index 3150b447..496138a5 100644 --- a/src/variables.rs +++ b/src/variables.rs @@ -71,17 +71,23 @@ impl TryFrom for i32 { } } -impl TryFrom for bool { - type Error = Error; - - fn try_from(value: Value) -> Result { +// Coercions from a value to a boolean always succeed, so every value +// can be used in an `if` statement. C does things that way, so how +// could it possibly be a bad idea? +impl From for bool { + fn from(value: Value) -> Self { match value { - Value::Bool(b) => Ok(b), - Value::Abool(b) => Ok(b.into()), - _ => Err(Error { - kind: ErrorKind::TypeError(format!("Expected bool, got {}", value)), - position: 0..0, - }), + // Booleans and abooleans have a trivial conversion. + Value::Bool(b) => b, + Value::Abool(b) => b.into(), + // The empty string is falsey, other strings are truthy. + Value::Str(s) => s.len() != 0, + // 0 is falsey, nonzero is truthy. + Value::Int(x) => x != 0, + // And nul is truthy as a symbol of the fact that the + // deep, fundamental truth of this world is nothing but + // the eternal void. + Value::Nul => true, } } }