From 109c77eeb2e3f81e8bd45323777acb4000020181 Mon Sep 17 00:00:00 2001 From: Alexander Bethel Date: Sun, 23 May 2021 18:46:42 -0500 Subject: [PATCH 1/4] Implement more statements Added variable declaration, `if` statements, `loop` statements, variable assignment, and variable banning to go along with printing (which was already implemented). We still need function declarations, brainfuck declarations, function calls, and the control flow operators "break" and "hopback". --- src/interpret.rs | 76 +++++++++++++++++++++++++++++++++++++++++------- src/variables.rs | 26 ++++++++++------- 2 files changed, 81 insertions(+), 21 deletions(-) diff --git a/src/interpret.rs b/src/interpret.rs index 051b092..3565b62 100644 --- a/src/interpret.rs +++ b/src/interpret.rs @@ -21,7 +21,6 @@ use crate::{ 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. @@ -82,13 +81,13 @@ 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 @@ -117,11 +116,66 @@ impl Scope { match stmt { Stmt::Print(expr) => { println!("{}", self.eval_expr(expr)?); - Ok(()) } - _ => { - todo!() + Stmt::VariableDeclaration { iden, init } => { + self.variables.insert( + iden.0.clone(), + Variable { + melo: false, + value: match init { + Some(init) => self.eval_expr(init)?, + None => Value::Nul, + }, + }, + ); + } + Stmt::FunctionDeclaration { + iden: _, + args: _, + body: _, + } => todo!(), + Stmt::BfFDeclaration { iden: _, body: _ } => todo!(), + Stmt::If { cond, body } => { + if self.eval_expr(cond)?.into() { + self.eval_items(body)?; + } + } + Stmt::FunctionCall { iden: _, args: _ } => todo!(), + Stmt::Loop { body } => { + loop { + // For now, loops run forever until they reach an + // error. + self.eval_items(body)?; + } + } + Stmt::VarAssignment { iden, value } => { + let value = self.eval_expr(value)?; + let record = self.variables.get_mut(&iden.0).ok_or_else(|| Error { + kind: ErrorKind::UnknownVariable(iden.0.clone()), + position: 0..0, + })?; + + if record.melo { + return Err(Error { + kind: ErrorKind::MeloVariable(iden.0.clone()), + position: 0..0, + }); + } + + record.value = value; + } + Stmt::Break => todo!(), + Stmt::HopBack => todo!(), + Stmt::Melo(iden) => { + let record = self.variables.get_mut(&iden.0).ok_or_else(|| Error { + kind: ErrorKind::UnknownVariable(iden.0.clone()), + position: 0..0, + })?; + + record.melo = true; } } + + Ok(()) } } diff --git a/src/variables.rs b/src/variables.rs index 3150b44..496138a 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, } } } From 669f379700874bbe8998794926ca5eb412d01695 Mon Sep 17 00:00:00 2001 From: Alexander Bethel Date: Tue, 25 May 2021 13:26:01 -0500 Subject: [PATCH 2/4] Better abstractions, implement scoping rules --- src/interpret.rs | 166 +++++++++++++++++++++++++++++------------------ src/main.rs | 6 +- src/repl.rs | 6 +- 3 files changed, 109 insertions(+), 69 deletions(-) diff --git a/src/interpret.rs b/src/interpret.rs index 3565b62..0bf4fec 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,33 +16,49 @@ 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, which serves +/// as a context in which expressions can be evaluated. +#[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. +} + +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. pub fn eval_items(&mut self, items: &[Item]) -> Result { - items + let init_depth = self.stack.len(); + + self.stack.push(Default::default()); + let res = items .iter() .map(|item| self.eval_item(item)) - .try_fold(Value::Nul, |_, result| result) + .try_fold(Value::Nul, |_, result| result); + self.stack.pop(); + + // Invariant: stack size must have net 0 change. + debug_assert_eq!(self.stack.len(), init_depth); + res } /// Evaluate a single Item, returning its value or an error. @@ -89,25 +105,7 @@ impl Scope { } 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(), }) } @@ -118,14 +116,19 @@ impl Scope { println!("{}", self.eval_expr(expr)?); } Stmt::VariableDeclaration { iden, init } => { - self.variables.insert( + 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: match init { - Some(init) => self.eval_expr(init)?, - None => Value::Nul, - }, + value: init, }, ); } @@ -149,33 +152,70 @@ impl Scope { } } Stmt::VarAssignment { iden, value } => { - let value = self.eval_expr(value)?; - let record = self.variables.get_mut(&iden.0).ok_or_else(|| Error { - kind: ErrorKind::UnknownVariable(iden.0.clone()), - position: 0..0, - })?; - - if record.melo { - return Err(Error { - kind: ErrorKind::MeloVariable(iden.0.clone()), - position: 0..0, - }); - } - - record.value = value; + self.get_var_mut(&iden.0)?.value = self.eval_expr(value)?; } Stmt::Break => todo!(), Stmt::HopBack => todo!(), Stmt::Melo(iden) => { - let record = self.variables.get_mut(&iden.0).ok_or_else(|| Error { - kind: ErrorKind::UnknownVariable(iden.0.clone()), - position: 0..0, - })?; - - record.melo = true; + self.get_var_mut(&iden.0)?.melo = true; } } Ok(()) } + + /// 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> { + // FIXME: This function is almost exactly the same as get_var. + 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 94626c8..2cebb37 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 58ac398..a442a97 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!( From ec81ead3ea4d0c3cf3d2cddfdc5720c34ebcd9e2 Mon Sep 17 00:00:00 2001 From: Alexander Bethel Date: Tue, 25 May 2021 21:22:38 -0500 Subject: [PATCH 3/4] Implement `break` and `hopback` statements --- src/error.rs | 1 + src/interpret.rs | 72 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/src/error.rs b/src/error.rs index 3d3b94b..c994bbd 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 0bf4fec..1473d68 100644 --- a/src/interpret.rs +++ b/src/interpret.rs @@ -34,6 +34,18 @@ struct Scope { // other information. } +/// The result of successfully executing a set of statements. +enum ControlFlow { + /// The statements evaluated to this value. + Value(Value), + + /// A "break" statement occurred at the top level. + Break, + + /// A "hopback" statement occurred at the top level. + Hopback, +} + impl ExecEnv { /// Create a new Scope with no predefined variable definitions or /// other information. @@ -47,25 +59,42 @@ impl ExecEnv { /// value of the last Item evaluated, or an error if one or more /// of the Items failed to evaluate. pub fn eval_items(&mut self, items: &[Item]) -> Result { + match self.eval_items_cf(items)? { + ControlFlow::Value(v) => Ok(v), + ControlFlow::Break | ControlFlow::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 reports "break" and "hopback" + /// exit codes as normal conditions in a ControlFlow enum. + fn eval_items_cf(&mut self, items: &[Item]) -> Result { let init_depth = self.stack.len(); self.stack.push(Default::default()); - let res = items - .iter() - .map(|item| self.eval_item(item)) - .try_fold(Value::Nul, |_, result| result); + let mut final_result = Ok(ControlFlow::Value(Value::Nul)); + for item in items { + final_result = self.eval_item(item); + if !matches!(final_result, Ok(ControlFlow::Value(_))) { + break; + } + } self.stack.pop(); // Invariant: stack size must have net 0 change. debug_assert_eq!(self.stack.len(), init_depth); - res + 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| ControlFlow::Value(v)), + Item::Stmt(stmt) => self.eval_stmt(stmt), } } @@ -110,7 +139,7 @@ impl ExecEnv { } /// 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)?); @@ -140,28 +169,33 @@ impl ExecEnv { Stmt::BfFDeclaration { iden: _, body: _ } => todo!(), Stmt::If { cond, body } => { if self.eval_expr(cond)?.into() { - self.eval_items(body)?; + return self.eval_items_cf(body); } } Stmt::FunctionCall { iden: _, args: _ } => todo!(), - Stmt::Loop { body } => { - loop { - // For now, loops run forever until they reach an - // error. - self.eval_items(body)?; + Stmt::Loop { body } => loop { + let res = self.eval_items_cf(body)?; + match res { + ControlFlow::Value(_) => {} + ControlFlow::Break => break, + ControlFlow::Hopback => continue, } - } + }, Stmt::VarAssignment { iden, value } => { self.get_var_mut(&iden.0)?.value = self.eval_expr(value)?; } - Stmt::Break => todo!(), - Stmt::HopBack => todo!(), + Stmt::Break => { + return Ok(ControlFlow::Break); + } + Stmt::HopBack => { + return Ok(ControlFlow::Hopback); + } Stmt::Melo(iden) => { self.get_var_mut(&iden.0)?.melo = true; } } - Ok(()) + Ok(ControlFlow::Value(Value::Nul)) } /// Get a shared reference to the value of a variable. Throw an From c906366e3fd2756b016892f9307f96a6616dd6e5 Mon Sep 17 00:00:00 2001 From: Alex Bethel Date: Tue, 25 May 2021 21:55:02 -0500 Subject: [PATCH 4/4] Improve name & documentation accuracy Renamed ControlFlow -> HaltStatus because that's what the enum really is -- a status on why something halted. Also reviewed `interpret.rs`'s documentation and fixed a few things that were out of date. --- src/interpret.rs | 63 +++++++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/src/interpret.rs b/src/interpret.rs index 1473d68..9fe6371 100644 --- a/src/interpret.rs +++ b/src/interpret.rs @@ -24,8 +24,8 @@ pub struct ExecEnv { stack: Vec, } -/// A set of visible variable and function definitions, which serves -/// as a context in which expressions can be evaluated. +/// A set of visible variable and function definitions in a single +/// stack frame. #[derive(Default)] struct Scope { /// The mapping from variable names to values. @@ -34,15 +34,17 @@ struct Scope { // other information. } -/// The result of successfully executing a set of statements. -enum ControlFlow { - /// The statements evaluated to this value. +/// 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 at the top level. + /// A `break` statement occurred and was not caught by a `loop` + /// statement. Break, - /// A "hopback" statement occurred at the top level. + /// A `hopback` statement occurred and was not caught by a `loop` + /// statement. Hopback, } @@ -57,11 +59,12 @@ impl ExecEnv { /// 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. + /// 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 { - match self.eval_items_cf(items)? { - ControlFlow::Value(v) => Ok(v), - ControlFlow::Break | ControlFlow::Hopback => Err(Error { + 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, @@ -70,16 +73,19 @@ impl ExecEnv { } } - /// The same as `eval_items`, but reports "break" and "hopback" - /// exit codes as normal conditions in a ControlFlow enum. - fn eval_items_cf(&mut self, items: &[Item]) -> Result { + /// 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(ControlFlow::Value(Value::Nul)); + let mut final_result = Ok(HaltStatus::Value(Value::Nul)); for item in items { final_result = self.eval_item(item); - if !matches!(final_result, Ok(ControlFlow::Value(_))) { + if !matches!(final_result, Ok(HaltStatus::Value(_))) { break; } } @@ -91,9 +97,9 @@ impl ExecEnv { } /// 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).map(|v| ControlFlow::Value(v)), + Item::Expr(expr) => self.eval_expr(expr).map(|v| HaltStatus::Value(v)), Item::Stmt(stmt) => self.eval_stmt(stmt), } } @@ -139,7 +145,7 @@ impl ExecEnv { } /// Perform the action indicated by a statement. - fn eval_stmt(&mut self, stmt: &Stmt) -> Result { + fn eval_stmt(&mut self, stmt: &Stmt) -> Result { match stmt { Stmt::Print(expr) => { println!("{}", self.eval_expr(expr)?); @@ -169,33 +175,33 @@ impl ExecEnv { Stmt::BfFDeclaration { iden: _, body: _ } => todo!(), Stmt::If { cond, body } => { if self.eval_expr(cond)?.into() { - return self.eval_items_cf(body); + return self.eval_items_hs(body); } } Stmt::FunctionCall { iden: _, args: _ } => todo!(), Stmt::Loop { body } => loop { - let res = self.eval_items_cf(body)?; + let res = self.eval_items_hs(body)?; match res { - ControlFlow::Value(_) => {} - ControlFlow::Break => break, - ControlFlow::Hopback => continue, + 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(ControlFlow::Break); + return Ok(HaltStatus::Break); } Stmt::HopBack => { - return Ok(ControlFlow::Hopback); + return Ok(HaltStatus::Hopback); } Stmt::Melo(iden) => { self.get_var_mut(&iden.0)?.melo = true; } } - Ok(ControlFlow::Value(Value::Nul)) + Ok(HaltStatus::Value(Value::Nul)) } /// Get a shared reference to the value of a variable. Throw an @@ -229,7 +235,8 @@ impl ExecEnv { /// 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> { - // FIXME: This function is almost exactly the same as get_var. + // 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()