From dcb8a03bed16bbc6a9c77e36490de08e3060bf36 Mon Sep 17 00:00:00 2001 From: Ochir Date: Wed, 3 Jan 2024 19:01:59 +0000 Subject: [PATCH 1/9] Working on interpreter. Need to refactor parser to stop evaluating expressions and work on representations for Statements --- src/app.rs | 25 ++++++++-------------- src/interpreter.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 ++- src/parse.rs | 48 +++++++++++++++++++++++------------------- src/token.rs | 4 +++- src/ui.rs | 20 +++++++++++------- 6 files changed, 105 insertions(+), 47 deletions(-) create mode 100644 src/interpreter.rs diff --git a/src/app.rs b/src/app.rs index 7a3fc44..93ff9de 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,10 +1,3 @@ -use std::{ - collections::HashMap, - f64::consts::{E, PI}, -}; - -const CONSTS: [(char, f64); 2] = [('p', PI), ('e', E)]; - use ratatui::{ style::{Color, Style}, widgets::{Block, Borders, Padding}, @@ -12,6 +5,7 @@ use ratatui::{ use tui_textarea::{Input, Key, TextArea}; use crate::{ + interpreter::Interpreter, parse::{Expr, ParseErr, Parser}, token::{Token, Tokenizer}, }; @@ -29,7 +23,7 @@ pub struct App<'ta> { pub input: TextArea<'ta>, pub input_type: InputType, pub output: Option>, - pub stored_vals: HashMap, + pub interpreter: Interpreter, pub expr_history: Vec, pub expr_selector: usize, pub should_quit: bool, @@ -38,12 +32,11 @@ pub struct App<'ta> { impl<'ta> App<'ta> { pub fn new() -> Self { - let stored_vals = HashMap::from_iter(CONSTS); Self { input: textarea(None, None, None), input_type: InputType::Expr, output: None, - stored_vals, + interpreter: Interpreter::new(), expr_history: Vec::new(), expr_selector: 0, should_quit: false, @@ -51,8 +44,8 @@ impl<'ta> App<'ta> { } } - pub fn reset_vals(&mut self) { - self.stored_vals = HashMap::from_iter(CONSTS); + pub fn reset_vars(&mut self) { + self.interpreter.reset_vars(); } pub fn reset_exprs(&mut self) { @@ -66,7 +59,7 @@ impl<'ta> App<'ta> { if let Key::Char(c) = input.key { if c.is_ascii_alphabetic() { let output = self.output.as_ref().unwrap().as_ref().unwrap(); - self.stored_vals.insert(c, *output); + self.interpreter.save_variable(c, *output); } else { self.output = Some(Err(ParseErr::new( Token::Var(c), @@ -86,14 +79,14 @@ impl<'ta> App<'ta> { let mut tokens = Tokenizer::new(input.chars().collect::>().as_slice()).collect::>(); tokens.push(Token::Eoe); - let res = Parser::new(tokens, self.stored_vals.clone()).parse(); + let res = Parser::new(tokens, self.interpreter.vars_into()).parse(); let output = match res { Ok(expr) => { let val = expr.eval(); if !self.expr_history.contains(&expr) { self.expr_history.push(expr); } - if self.expr_selector == self.stored_vals.len() { + if self.expr_selector == self.interpreter.vars_len() { self.expr_selector += 1; } Ok(val) @@ -102,7 +95,7 @@ impl<'ta> App<'ta> { }; self.output = Some(output); if let Some(Ok(ans)) = self.output { - self.stored_vals.insert('q', ans); + self.interpreter.save_variable('q', ans); } self.input = textarea(None, None, None); } diff --git a/src/interpreter.rs b/src/interpreter.rs new file mode 100644 index 0000000..b79ae17 --- /dev/null +++ b/src/interpreter.rs @@ -0,0 +1,52 @@ +use std::{ + collections::HashMap, + f64::consts::{E, PI}, +}; + +use crate::parse::Expr; + +const CONSTS: [(char, f64); 2] = [('p', PI), ('e', E)]; + +#[derive(Debug)] +pub struct Interpreter { + vars: HashMap, +} + +#[derive(Debug)] +// I don't think this is the right terminology so feel free to LMK +// if you can think of something better +pub enum Stmt { + Expr(Expr), + Fn(Fn), +} + +#[derive(Debug)] +pub struct Fn {} + +impl Interpreter { + pub fn new() -> Self { + Self { + vars: HashMap::from_iter(CONSTS), + } + } + + pub fn reset_vars(&mut self) { + self.vars = HashMap::from_iter(CONSTS); + } + + pub fn save_variable(&mut self, var: char, val: f64) { + self.vars.insert(var, val); + } + + pub fn vars_into(&self) -> HashMap { + self.vars.clone() + } + + pub fn vars(&self) -> &HashMap { + &self.vars + } + + pub fn vars_len(&self) -> usize { + self.vars.len() + } +} diff --git a/src/lib.rs b/src/lib.rs index 732eeed..963fced 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ use tui::Tui; mod app; mod event; +mod interpreter; mod parse; mod token; mod tui; @@ -64,7 +65,7 @@ fn update(app: &mut App, key_event: KeyEvent) { app.reset_exprs(); } KeyCode::Char('v') if key_event.modifiers.contains(KeyModifiers::CONTROL) => { - app.reset_vals(); + app.reset_vars(); } KeyCode::Char('h') if key_event.modifiers.contains(KeyModifiers::CONTROL) => { app.popup = Some(Popup::Funcs); diff --git a/src/parse.rs b/src/parse.rs index 6bcbe07..bdcdce6 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -146,11 +146,11 @@ impl Parser { } fn at_end(&self) -> bool { - self.peek() == Token::Eoe + *self.peek() == Token::Eoe } fn previous(&self) -> Token { - self.tokens[self.current - 1] + self.tokens[self.current - 1].clone() } fn advance(&mut self) -> Token { @@ -160,19 +160,19 @@ impl Parser { self.previous() } - fn peek(&self) -> Token { - self.tokens[self.current] + fn peek(&self) -> &Token { + &self.tokens[self.current] } - fn check(&self, token: Token) -> bool { + fn check(&self, token: &Token) -> bool { if self.at_end() { return false; } - self.peek() == token + *self.peek() == *token } fn consume(&mut self, token: Token, msg: &'static str) -> Result { - if self.check(token) { + if self.check(&token) { Ok(self.advance()) } else { Err(ParseErr::new(token, msg)) @@ -191,7 +191,7 @@ impl Parser { fn term(&mut self) -> Result { let mut expr = self.factor()?; - while self.peek() == Token::Plus || self.peek() == Token::Minus { + while *self.peek() == Token::Plus || *self.peek() == Token::Minus { let operator = self.advance(); let right = self.factor()?; expr = Expr::Binary(Box::new(expr), operator, Box::new(right)); @@ -201,7 +201,10 @@ impl Parser { fn factor(&mut self) -> Result { let mut expr = self.exponent()?; - while self.peek() == Token::Div || self.peek() == Token::Mult || self.peek() == Token::Mod { + while *self.peek() == Token::Div + || *self.peek() == Token::Mult + || *self.peek() == Token::Mod + { let operator = self.advance(); let right = self.exponent()?; expr = Expr::Binary(Box::new(expr), operator, Box::new(right)); @@ -211,7 +214,7 @@ impl Parser { fn exponent(&mut self) -> Result { let mut expr = self.negative()?; - while self.peek() == Token::Power { + while *self.peek() == Token::Power { self.advance(); let right = self.negative()?; expr = Expr::Exponent(Box::new(expr), Box::new(right)); @@ -220,7 +223,7 @@ impl Parser { } fn negative(&mut self) -> Result { - if self.peek() == Token::Minus { + if *self.peek() == Token::Minus { self.advance(); let right = self.negative()?; Ok(Expr::Negative(Box::new(right))) @@ -230,32 +233,32 @@ impl Parser { } fn primary(&mut self) -> Result { - let token = self.peek(); - if let Token::Num(num) = token { + if let Token::Num(num) = *self.peek() { self.advance(); return Ok(Expr::Num(num)); } - if let Token::LParen = token { + if let Token::LParen = *self.peek() { self.advance(); let expr = Box::new(self.expression()?); self.consume(Token::RParen, "Missing closing parentheses")?; return Ok(Expr::Grouping(expr)); } - if let Token::Pipe = token { + if let Token::Pipe = *self.peek() { self.advance(); let expr = Box::new(self.expression()?); self.consume(Token::Pipe, "Missing closing pipe")?; return Ok(Expr::Abs(expr)); } - if let Token::Var(var) = token { + if let Token::Var(var) = *self.peek() { + // We need to stop evaluating variables here and move that to the interpreter if let Some(&num) = self.values.get(&var) { self.advance(); return Ok(Expr::Num(num)); } else { - return Err(ParseErr::new(token, "Unknown variable")); + return Err(ParseErr::new(self.peek().clone(), "Unknown variable")); } } - if let Token::Func(func) = token { + if let Token::Func(func) = *self.peek() { self.advance(); let func = match func { SIN => Func::Sin, @@ -269,17 +272,20 @@ impl Parser { if let Token::Num(base) = self.advance() { Func::Log(base) } else { - return Err(ParseErr::new(token, "Missing base for log function")); + return Err(ParseErr::new( + self.peek().clone(), + "Missing base for log function", + )); } } - _ => return Err(ParseErr::new(token, "Unknown function")), + _ => return Err(ParseErr::new(self.peek().clone(), "Unknown function")), }; self.consume(Token::LParen, "Missing opening parentheses")?; let arg = Box::new(self.expression()?); self.consume(Token::RParen, "Missing closing parentheses")?; return Ok(Expr::Func(func, arg)); } - Err(ParseErr::new(token, "Expected expression")) + Err(ParseErr::new(self.peek().clone(), "Expected expression")) } } diff --git a/src/token.rs b/src/token.rs index 822da68..234f935 100644 --- a/src/token.rs +++ b/src/token.rs @@ -1,11 +1,12 @@ use crate::{inner_write, parse::FUNCS}; -#[derive(PartialEq, Debug, Clone, Copy)] +#[derive(PartialEq, Debug, Clone)] pub enum Token { Num(f64), Var(char), // Change this to a string and combine functions and vars into one table and // dynamimcally use them Func(&'static str), + Ident(String), Pipe, Mod, Div, @@ -24,6 +25,7 @@ impl std::fmt::Display for Token { Token::Num(num) => inner_write(num, f), Token::Var(var) => inner_write(var, f), Token::Func(func) => inner_write(func, f), + Token::Ident(ident) => inner_write(ident, f), Token::Pipe => inner_write('|', f), Token::Mod => inner_write('%', f), Token::Mult => inner_write('*', f), diff --git a/src/ui.rs b/src/ui.rs index e763c4b..993ad76 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -43,14 +43,18 @@ pub fn render(app: &mut App, f: &mut Frame) { .block(title_block); f.render_widget(title, value_chunks[0]); } - let list_items = app.stored_vals.iter().fold(vec![], |mut acc, (i, val)| { - let list_item = ListItem::new(Line::from(Span::styled( - format!("{}: {}", i, val), - Style::default().fg(Color::LightYellow), - ))); - acc.push(list_item); - acc - }); + let list_items = app + .interpreter + .vars() + .iter() + .fold(vec![], |mut acc, (i, val)| { + let list_item = ListItem::new(Line::from(Span::styled( + format!("{}: {}", i, val), + Style::default().fg(Color::LightYellow), + ))); + acc.push(list_item); + acc + }); let output_list = List::new(list_items).block(Block::default().borders(Borders::LEFT)); f.render_widget(output_list, value_chunks[1]); } From a88082837e3ec399c1fe9b5c74ee58f19863bbb8 Mon Sep 17 00:00:00 2001 From: Ochir Date: Wed, 3 Jan 2024 19:18:09 +0000 Subject: [PATCH 2/9] Remove function --- src/app.rs | 7 +++---- src/interpreter.rs | 10 ---------- src/parse.rs | 7 +++---- src/token.rs | 20 +++++++------------- 4 files changed, 13 insertions(+), 31 deletions(-) diff --git a/src/app.rs b/src/app.rs index 93ff9de..ea94bff 100644 --- a/src/app.rs +++ b/src/app.rs @@ -89,15 +89,14 @@ impl<'ta> App<'ta> { if self.expr_selector == self.interpreter.vars_len() { self.expr_selector += 1; } + // Only reset input if we successfully evaluate + self.input = textarea(None, None, None); + self.interpreter.save_variable('q', val); Ok(val) } Err(err) => Err(err), }; self.output = Some(output); - if let Some(Ok(ans)) = self.output { - self.interpreter.save_variable('q', ans); - } - self.input = textarea(None, None, None); } // true == select up | false == select down diff --git a/src/interpreter.rs b/src/interpreter.rs index b79ae17..2667234 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -3,8 +3,6 @@ use std::{ f64::consts::{E, PI}, }; -use crate::parse::Expr; - const CONSTS: [(char, f64); 2] = [('p', PI), ('e', E)]; #[derive(Debug)] @@ -12,14 +10,6 @@ pub struct Interpreter { vars: HashMap, } -#[derive(Debug)] -// I don't think this is the right terminology so feel free to LMK -// if you can think of something better -pub enum Stmt { - Expr(Expr), - Fn(Fn), -} - #[derive(Debug)] pub struct Fn {} diff --git a/src/parse.rs b/src/parse.rs index bdcdce6..6f443a6 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -10,8 +10,6 @@ const TANH: &str = "tanh"; const LOG: &str = "log"; const LN: &str = "ln"; -pub const FUNCS: [&str; 8] = [COS, COSH, SIN, SINH, TAN, TANH, LOG, LN]; - #[derive(Debug)] pub struct Parser { tokens: Vec, @@ -258,9 +256,10 @@ impl Parser { return Err(ParseErr::new(self.peek().clone(), "Unknown variable")); } } - if let Token::Func(func) = *self.peek() { + if let Token::Ident(func) = self.peek() { + let func = func.to_owned(); self.advance(); - let func = match func { + let func = match func.as_str() { SIN => Func::Sin, SINH => Func::Sinh, COS => Func::Cos, diff --git a/src/token.rs b/src/token.rs index 234f935..5037d1f 100644 --- a/src/token.rs +++ b/src/token.rs @@ -1,11 +1,12 @@ -use crate::{inner_write, parse::FUNCS}; +use crate::inner_write; #[derive(PartialEq, Debug, Clone)] pub enum Token { Num(f64), Var(char), // Change this to a string and combine functions and vars into one table and // dynamimcally use them - Func(&'static str), + // This must go bye-bye and become an Ident + Fn, Ident(String), Pipe, Mod, @@ -24,7 +25,7 @@ impl std::fmt::Display for Token { match self { Token::Num(num) => inner_write(num, f), Token::Var(var) => inner_write(var, f), - Token::Func(func) => inner_write(func, f), + Token::Fn => inner_write("fn", f), Token::Ident(ident) => inner_write(ident, f), Token::Pipe => inner_write('|', f), Token::Mod => inner_write('%', f), @@ -106,17 +107,10 @@ impl<'a> Iterator for Tokenizer<'a> { } if func.len() == 1 { Token::Var(*next) + } else if func == "fn" { + Token::Fn } else { - // Tokenizer shouldn't really be doing this but - // I don't want to have `String` in the enum because - // we'll lose the `Copy` trait. - let name = if let Some(pos) = FUNCS.iter().position(|f| *f == func.as_str()) - { - FUNCS[pos] - } else { - "unknown" - }; - Token::Func(name) + Token::Ident(func) } } _ => return None, // Handle wrong tokens a different way From faba070d4e84fe79d963769a90cd0ed6070c6e34 Mon Sep 17 00:00:00 2001 From: Ochir Date: Thu, 4 Jan 2024 23:14:32 +0000 Subject: [PATCH 3/9] Mostly working fine. Might remove the Env abstraction as it seems unnecessary --- src/app.rs | 104 ++++++++++++-------- src/interpreter.rs | 240 ++++++++++++++++++++++++++++++++++++++++++--- src/lib.rs | 4 +- src/parse.rs | 216 +++++++++++++++++++++++----------------- src/token.rs | 18 +--- src/ui.rs | 38 ++++--- 6 files changed, 439 insertions(+), 181 deletions(-) diff --git a/src/app.rs b/src/app.rs index ea94bff..652369e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,11 +1,13 @@ +use std::error::Error; + use ratatui::{ style::{Color, Style}, widgets::{Block, Borders, Padding}, }; -use tui_textarea::{Input, Key, TextArea}; +use tui_textarea::{Input, TextArea}; use crate::{ - interpreter::Interpreter, + interpreter::{Interpreter, Stmt, Value}, parse::{Expr, ParseErr, Parser}, token::{Token, Tokenizer}, }; @@ -16,13 +18,13 @@ pub enum InputType { } pub enum Popup { - Funcs, + Help, } pub struct App<'ta> { pub input: TextArea<'ta>, pub input_type: InputType, - pub output: Option>, + pub output: Option>>, pub interpreter: Interpreter, pub expr_history: Vec, pub expr_selector: usize, @@ -53,50 +55,66 @@ impl<'ta> App<'ta> { } pub fn input(&mut self, input: Input) { + self.input.input(input); + } + + pub fn eval(&mut self) { match self.input_type { - InputType::Expr => self.input.input(input), - InputType::SetVar => { - if let Key::Char(c) = input.key { - if c.is_ascii_alphabetic() { - let output = self.output.as_ref().unwrap().as_ref().unwrap(); - self.interpreter.save_variable(c, *output); - } else { - self.output = Some(Err(ParseErr::new( - Token::Var(c), - "Variable must a single alphabetic character", - ))); + InputType::Expr => { + let input = &self.input.lines()[0]; + let mut tokens = Tokenizer::new(input.chars().collect::>().as_slice()) + .collect::>(); + tokens.push(Token::Eoe); + let res = Parser::new(tokens).parse(); + match res { + Ok(expr) => { + match expr { + Stmt::Expr(expr) => { + match self.interpreter.interpret_expr(&expr) { + Ok(val) => { + if !self.expr_history.contains(&expr) { + self.expr_history.push(expr); + } + if self.expr_selector == self.interpreter.vars_len() { + self.expr_selector += 1; + } + // Only reset input if we successfully evaluate + self.input = textarea(None, None, None); + self.interpreter.define("q".to_string(), Value::Num(val)); + self.output = Some(Ok(val)); + } + Err(err) => self.output = Some(Err(Box::new(err))), + } + } + Stmt::Fn(name, parameters, body) => { + self.interpreter.declare_function(name, parameters, body); + self.input = textarea(None, None, None); + } + } } + Err(err) => self.output = Some(Err(Box::new(err))), + }; + } + InputType::SetVar => { + let name = self.input.lines()[0].trim().to_string(); + if name.is_empty() { + self.output = Some(Err(Box::new(ParseErr::new( + Token::Ident(name), + "Variable name cannot be empty", + )))); + } else if !name.chars().all(|c| c.is_ascii_alphabetic()) { + self.output = Some(Err(Box::new(ParseErr::new( + Token::Ident(name), + "Variable names must be letters", + )))); + } else { + let output = self.output.as_ref().unwrap().as_ref().unwrap(); + self.interpreter.define(name, Value::Num(*output)); self.input = textarea(None, None, None); self.input_type = InputType::Expr; } - true } - }; - } - - pub fn eval(&mut self) { - let input = &self.input.lines()[0]; - let mut tokens = - Tokenizer::new(input.chars().collect::>().as_slice()).collect::>(); - tokens.push(Token::Eoe); - let res = Parser::new(tokens, self.interpreter.vars_into()).parse(); - let output = match res { - Ok(expr) => { - let val = expr.eval(); - if !self.expr_history.contains(&expr) { - self.expr_history.push(expr); - } - if self.expr_selector == self.interpreter.vars_len() { - self.expr_selector += 1; - } - // Only reset input if we successfully evaluate - self.input = textarea(None, None, None); - self.interpreter.save_variable('q', val); - Ok(val) - } - Err(err) => Err(err), - }; - self.output = Some(output); + } } // true == select up | false == select down @@ -118,7 +136,7 @@ impl<'ta> App<'ta> { self.input = textarea(Some(string), None, None); } - pub fn save_result(&mut self) { + pub fn save_result_input(&mut self) { self.input = textarea( None, Some("Select one letter"), diff --git a/src/interpreter.rs b/src/interpreter.rs index 2667234..45fccd4 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -1,42 +1,256 @@ use std::{ + cell::RefCell, collections::HashMap, + error::Error, f64::consts::{E, PI}, + fmt::Display, + ops::Neg, + rc::Rc, }; -const CONSTS: [(char, f64); 2] = [('p', PI), ('e', E)]; +use crate::{ + inner_write, + parse::{Expr, Func}, + token::Token, +}; #[derive(Debug)] pub struct Interpreter { - vars: HashMap, + env: Rc>, +} + +#[derive(Debug, PartialEq)] +pub enum Stmt { + Expr(Expr), + Fn(String, Vec, Expr), +} + +#[derive(Debug, Clone)] +pub enum Value { + Fn(Function), + Num(f64), +} + +#[derive(Debug, Clone)] +pub struct Function { + closure: Rc>, + parameters: Vec, + arity: usize, + body: Expr, } #[derive(Debug)] -pub struct Fn {} +pub enum InterpretError { + UnknownVariable(String), + UnknownFunction(String), + UnInvokedFunction(String), + WrongArity(String, usize, usize), +} + +#[derive(Debug)] +pub struct Env { + vals: HashMap, +} + +impl Function { + fn new(parameters: Vec, body: Expr, closure: Rc>) -> Self { + Self { + arity: parameters.len(), + parameters, + body, + closure, + } + } + + fn call(&self, args: Vec) -> Result { + let mut interpreter = Interpreter::with_env(self.closure.clone()); + args.into_iter() + .enumerate() + .for_each(|(i, arg)| interpreter.define(self.parameters[i].clone(), arg)); + interpreter.interpret_expr(&self.body) + } +} + +impl Env { + fn global() -> Self { + Self { + // I think we actually don't need this. We would never access things + // outside of the function's closure + vals: Self::default_vars(), + } + } + + fn from_vals(vals: HashMap) -> Self { + Self { vals } + } + + fn insert(&mut self, var: String, val: Value) { + self.vals.insert(var, val); + } + + fn get(&self, var: &str) -> Option { + self.vals.get(var).cloned() + } + + fn reset(&mut self) { + self.vals = Self::default_vars() + } + + fn default_vars() -> HashMap { + HashMap::from_iter([ + ("p".to_string(), Value::Num(PI)), + ("e".to_string(), Value::Num(E)), + ]) + .clone() + } + + pub fn vars(&self) -> &HashMap { + &self.vals + } +} impl Interpreter { pub fn new() -> Self { Self { - vars: HashMap::from_iter(CONSTS), + env: Rc::new(RefCell::new(Env::global())), } } - pub fn reset_vars(&mut self) { - self.vars = HashMap::from_iter(CONSTS); + pub fn with_env(env: Rc>) -> Self { + Self { env } + } + + pub fn declare_function(&mut self, name: String, parameters: Vec, body: Expr) { + self.env.as_ref().borrow_mut().insert( + name, + Value::Fn(Function::new(parameters, body, self.env.clone())), + ); } - pub fn save_variable(&mut self, var: char, val: f64) { - self.vars.insert(var, val); + pub fn interpret_expr(&self, expr: &Expr) -> Result { + match expr { + Expr::Num(num) => Ok(*num), + Expr::Binary(left, operator, right) => { + let left = self.interpret_expr(left)?; + let right = self.interpret_expr(right)?; + let val = match operator { + Token::Plus => left + right, + Token::Minus => left - right, + Token::Mult => left * right, + Token::Div => left / right, + Token::Mod => left % right, + _ => unreachable!(), + }; + Ok(val) + } + Expr::Abs(expr) => Ok(self.interpret_expr(expr)?.abs()), + Expr::Grouping(expr) => self.interpret_expr(expr), + Expr::Negative(expr) => Ok(self.interpret_expr(expr)?.neg()), + Expr::Exponent(base, exponent) => Ok(self + .interpret_expr(base)? + .powf(self.interpret_expr(exponent)?)), + Expr::Call(name, args) => { + if let Some(Value::Fn(func)) = self.env.as_ref().borrow().get(name) { + if args.len() != func.arity { + Err(InterpretError::WrongArity( + name.to_owned(), + args.len(), + func.arity, + )) + } else { + let mut vals = vec![]; + for arg in args.iter() { + vals.push(Value::Num(self.interpret_expr(arg)?)); + } + func.call(vals) + } + } else { + Err(InterpretError::UnknownFunction(name.to_owned())) + } + } + Expr::Var(var) => { + if let Some(val) = self.env.as_ref().borrow().get(var) { + match val { + Value::Fn(_) => Err(InterpretError::UnInvokedFunction(var.clone())), + Value::Num(num) => Ok(num), + } + } else { + Err(InterpretError::UnknownVariable(var.clone())) + } + } + Expr::Func(func, arg) => { + let arg = self.interpret_expr(arg)?; + let val = match func { + Func::Sin => arg.sin(), + Func::Sinh => arg.sinh(), + Func::Cos => arg.cos(), + Func::Cosh => arg.cosh(), + Func::Tan => arg.tan(), + Func::Tanh => arg.tanh(), + Func::Ln => arg.ln(), + Func::Log(b) => arg.log(*b), + }; + Ok(val) + } + } + } + + pub fn reset_vars(&mut self) { + self.env.as_ref().borrow_mut().reset(); } - pub fn vars_into(&self) -> HashMap { - self.vars.clone() + pub fn define(&mut self, var: String, val: Value) { + self.env.as_ref().borrow_mut().insert(var, val); } - pub fn vars(&self) -> &HashMap { - &self.vars + pub fn env(&self) -> Rc> { + self.env.clone() } pub fn vars_len(&self) -> usize { - self.vars.len() + self.env.as_ref().borrow().vals.len() + } + + fn new_env(&mut self) { + let outer = self.env.clone(); + self.env = Rc::new(RefCell::new(Env::from_vals( + outer.as_ref().borrow().vals.clone(), + ))); + } +} + +impl Error for InterpretError {} + +impl Display for InterpretError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::UnknownVariable(c) => format!("Unknown variable {}", c), + Self::UnknownFunction(c) => format!("Unknown function {}", c), + Self::UnInvokedFunction(c) => format!("Uninvoked function {}", c), + Self::WrongArity(name, actual, expected) => format!( + "Function {} takes {} arguments but {} were provided", + name, expected, actual + ), + }, + ) + } +} + +impl Display for Value { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Num(num) => inner_write(num, f), + Self::Fn(func) => inner_write(func, f), + } + } +} + +impl Display for Function { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // TODO: This properly... + write!(f, "({}) {}", self.parameters.join(", "), self.body) } } diff --git a/src/lib.rs b/src/lib.rs index 963fced..ac9876d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,7 +58,7 @@ fn update(app: &mut App, key_event: KeyEvent) { } KeyCode::Char('s') if key_event.modifiers.contains(KeyModifiers::CONTROL) => { if app.output.as_ref().is_some_and(|o| o.is_ok()) { - app.save_result(); + app.save_result_input(); } } KeyCode::Char('e') if key_event.modifiers.contains(KeyModifiers::CONTROL) => { @@ -68,7 +68,7 @@ fn update(app: &mut App, key_event: KeyEvent) { app.reset_vars(); } KeyCode::Char('h') if key_event.modifiers.contains(KeyModifiers::CONTROL) => { - app.popup = Some(Popup::Funcs); + app.popup = Some(Popup::Help); } KeyCode::Char('x') if key_event.modifiers.contains(KeyModifiers::CONTROL) => { app.remove_expr(); diff --git a/src/parse.rs b/src/parse.rs index 6f443a6..26fb4be 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,6 +1,6 @@ -use std::{collections::HashMap, fmt::Display, ops::Neg}; +use std::{error::Error, fmt::Display}; -use crate::{inner_write, token::Token}; +use crate::{inner_write, interpreter::Stmt, token::Token}; const COS: &str = "cos"; const COSH: &str = "cosh"; const SIN: &str = "sin"; @@ -14,7 +14,6 @@ const LN: &str = "ln"; pub struct Parser { tokens: Vec, current: usize, - values: HashMap, } #[derive(Debug, PartialEq)] @@ -43,9 +42,14 @@ pub enum Expr { Negative(Box), Abs(Box), Exponent(Box, Box), + Call(String, Vec), Func(Func, Box), + // TODO: Change this to a String + Var(String), } +impl Error for ParseErr {} + impl Display for ParseErr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "ERROR: {}", self.msg) @@ -78,50 +82,26 @@ impl Display for Func { } impl Expr { - pub fn eval(&self) -> f64 { - match self { - Self::Num(num) => *num, - Self::Binary(left, operator, right) => { - let left = Self::eval(left); - let right = Self::eval(right); - match operator { - Token::Plus => left + right, - Token::Minus => left - right, - Token::Mult => left * right, - Token::Div => left / right, - Token::Mod => left % right, - _ => unreachable!(), - } - } - Self::Abs(expr) => Self::eval(expr).abs(), - Self::Grouping(expr) => Self::eval(expr), - Self::Negative(expr) => Self::eval(expr).neg(), - Self::Exponent(base, exponent) => Self::eval(base).powf(Self::eval(exponent)), - Self::Func(func, arg) => { - let arg = Self::eval(arg); - match func { - Func::Sin => arg.sin(), - Func::Sinh => arg.sinh(), - Func::Cos => arg.cos(), - Func::Cosh => arg.cosh(), - Func::Tan => arg.tan(), - Func::Tanh => arg.tanh(), - Func::Ln => arg.ln(), - Func::Log(b) => arg.log(*b), - } - } - } - } - pub fn format(&self) -> String { match self { Self::Num(num) => num.to_string(), Self::Negative(expr) => format!("-{}", expr.format()), Self::Grouping(expr) => format!("({})", expr.format()), Self::Abs(expr) => format!("|{}|", expr.format()), + Self::Var(var) => var.to_string(), Self::Binary(left, operator, right) => { format!("{}{}{}", left.format(), operator, right.format()) } + Self::Call(name, args) => { + format!( + "{}({})", + name, + args.iter() + .map(|a| a.format()) + .collect::>() + .join(", ") + ) + } Self::Exponent(base, exponent) => format!("{}^{}", base.format(), exponent.format()), Self::Func(func, argument) => format!("{}({})", func, argument.format()), } @@ -135,12 +115,8 @@ impl ParseErr { } impl Parser { - pub fn new(tokens: Vec, values: HashMap) -> Self { - Self { - tokens, - current: 0, - values, - } + pub fn new(tokens: Vec) -> Self { + Self { tokens, current: 0 } } fn at_end(&self) -> bool { @@ -179,8 +155,38 @@ impl Parser { } impl Parser { - pub fn parse(&mut self) -> Result { - self.expression() + pub fn parse(&mut self) -> Result { + match self.peek() { + Token::Fn => self.function(), + _ => Ok(Stmt::Expr(self.expression()?)), + } + } + + fn function(&mut self) -> Result { + self.advance(); + let name = match self.advance() { + Token::Ident(name) => name, + token => return Err(ParseErr::new(token, "Missing function name")), + }; + self.consume(Token::LParen, "Missing opening parentheses")?; + let mut parameters = Vec::new(); + if *self.peek() != Token::RParen { + loop { + let next = self.advance(); + if let Token::Ident(arg) = next { + parameters.push(arg); + } else { + return Err(ParseErr::new(next, "Expected argument")); + } + if *self.peek() != Token::Comma { + break; + } + self.advance(); + } + } + self.consume(Token::RParen, "Missing closing parentheses")?; + let expr = self.expression()?; + Ok(Stmt::Fn(name, parameters, expr)) } fn expression(&mut self) -> Result { @@ -226,10 +232,33 @@ impl Parser { let right = self.negative()?; Ok(Expr::Negative(Box::new(right))) } else { - self.primary() + self.call() } } + fn call(&mut self) -> Result { + let expr = self.primary()?; + + if let Expr::Var(name) = &expr { + if self.check(&Token::LParen) { + self.advance(); + let mut args = vec![]; + if !self.check(&Token::RParen) { + loop { + args.push(self.expression()?); + if !self.check(&Token::Comma) { + break; + } + self.advance(); + } + } + self.consume(Token::RParen, "Missing closing parentheses")?; + return Ok(Expr::Call(name.to_owned(), args)); + } + } + Ok(expr) + } + fn primary(&mut self) -> Result { if let Token::Num(num) = *self.peek() { self.advance(); @@ -247,15 +276,9 @@ impl Parser { self.consume(Token::Pipe, "Missing closing pipe")?; return Ok(Expr::Abs(expr)); } - if let Token::Var(var) = *self.peek() { - // We need to stop evaluating variables here and move that to the interpreter - if let Some(&num) = self.values.get(&var) { - self.advance(); - return Ok(Expr::Num(num)); - } else { - return Err(ParseErr::new(self.peek().clone(), "Unknown variable")); - } - } + // This is still assuming Ident == Func + // Needs to start treating ( as call expressions properly + // A solution for built in functions might be to treat them as a separate case entirely if let Token::Ident(func) = self.peek() { let func = func.to_owned(); self.advance(); @@ -277,7 +300,7 @@ impl Parser { )); } } - _ => return Err(ParseErr::new(self.peek().clone(), "Unknown function")), + _ => return Ok(Expr::Var(func)), }; self.consume(Token::LParen, "Missing opening parentheses")?; let arg = Box::new(self.expression()?); @@ -293,10 +316,10 @@ mod tests { use super::*; fn check(tokens: Vec, expected: Expr) { - let mut parser = Parser::new(tokens, HashMap::new()); + let mut parser = Parser::new(tokens); let expr = parser.parse().unwrap(); - assert_eq!(expr, expected,); + assert_eq!(expr, Stmt::Expr(expected)); } #[test] @@ -373,7 +396,7 @@ mod tests { #[test] fn test_missing_closing_paren() { let tokens = vec![Token::Minus, Token::LParen, Token::Num(5.0), Token::Eoe]; - if let Err(err) = Parser::new(tokens, HashMap::new()).parse() { + if let Err(err) = Parser::new(tokens).parse() { assert_eq!( err, ParseErr::new(Token::RParen, "Missing closing parentheses") @@ -384,43 +407,58 @@ mod tests { } #[test] - fn test_variable() { - let tokens = vec![Token::Var('a'), Token::Plus, Token::Num(3.0), Token::Eoe]; - let mut parser = Parser::new(tokens, HashMap::from_iter([('a', 1.0)])); - let expr = Expr::Binary( - Box::new(Expr::Num(1.0)), + fn test_function_mult_params() { + let tokens = vec![ + Token::Fn, + Token::Ident("foo".to_string()), + Token::LParen, + Token::Ident("x".to_string()), + Token::Comma, + Token::Ident("y".to_string()), + Token::RParen, + Token::Ident("x".to_string()), Token::Plus, - Box::new(Expr::Num(3.0)), + Token::Ident("y".to_string()), + Token::Eoe, + ]; + let expected = Stmt::Fn( + "foo".to_string(), + vec!["x".to_string(), "y".to_string()], + Expr::Binary( + Box::new(Expr::Var("x".to_string())), + Token::Plus, + Box::new(Expr::Var("y".to_string())), + ), ); - let expected = parser.parse().unwrap(); - - assert_eq!(expected, expr); + let stmt = Parser::new(tokens).parse().unwrap(); + assert_eq!(stmt, expected); } #[test] - fn simple_add_eval() { - let expr = Expr::Binary( - Box::new(Expr::Num(5.0)), + fn test_function_single_param() { + let tokens = vec![ + Token::Fn, + Token::Ident("foo".to_string()), + Token::LParen, + Token::Ident("y".to_string()), + Token::RParen, + Token::Ident("y".to_string()), Token::Plus, - Box::new(Expr::Num(1.0)), - ); - - assert_eq!(expr.eval(), 6.0); - } - - #[test] - fn test_negative() { - let expr = Expr::Binary( - Box::new(Expr::Grouping(Box::new(Expr::Binary( - Box::new(Expr::Num(5.0)), - Token::Minus, - Box::new(Expr::Num(3.0)), - )))), - Token::Mult, - Box::new(Expr::Negative(Box::new(Expr::Num(3.0)))), + Token::Num(1.0), + Token::Eoe, + ]; + let expected = Stmt::Fn( + "foo".to_string(), + vec!["y".to_string()], + Expr::Binary( + Box::new(Expr::Var("y".to_string())), + Token::Plus, + Box::new(Expr::Num(1.0)), + ), ); - assert_eq!(expr.eval(), -6.0); + let stmt = Parser::new(tokens).parse().unwrap(); + assert_eq!(stmt, expected); } } diff --git a/src/token.rs b/src/token.rs index 5037d1f..433e891 100644 --- a/src/token.rs +++ b/src/token.rs @@ -3,10 +3,8 @@ use crate::inner_write; #[derive(PartialEq, Debug, Clone)] pub enum Token { Num(f64), - Var(char), // Change this to a string and combine functions and vars into one table and - // dynamimcally use them - // This must go bye-bye and become an Ident Fn, + Comma, Ident(String), Pipe, Mod, @@ -24,8 +22,8 @@ impl std::fmt::Display for Token { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Token::Num(num) => inner_write(num, f), - Token::Var(var) => inner_write(var, f), Token::Fn => inner_write("fn", f), + Token::Comma => inner_write(',', f), Token::Ident(ident) => inner_write(ident, f), Token::Pipe => inner_write('|', f), Token::Mod => inner_write('%', f), @@ -62,6 +60,7 @@ impl<'a> Iterator for Tokenizer<'a> { ' ' => { return self.next(); } + ',' => Token::Comma, '|' => Token::Pipe, '/' => Token::Div, '+' => Token::Plus, @@ -105,9 +104,7 @@ impl<'a> Iterator for Tokenizer<'a> { func.push(self.input[self.index]); self.index += 1; } - if func.len() == 1 { - Token::Var(*next) - } else if func == "fn" { + if func == "fn" { Token::Fn } else { Token::Ident(func) @@ -167,11 +164,4 @@ mod tests { ] ); } - - #[test] - fn test_var() { - let str = "a + 3".chars().collect::>(); - let tokens = Tokenizer::new(str.as_slice()).collect::>(); - assert_eq!(tokens, vec![Token::Var('a'), Token::Plus, Token::Num(3.0)]); - } } diff --git a/src/ui.rs b/src/ui.rs index 993ad76..c636e86 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -31,30 +31,28 @@ pub fn render(app: &mut App, f: &mut Frame) { .direction(Direction::Vertical) .constraints([Constraint::Length(3), Constraint::Min(1)]) .split(layout[0]); - { - let title_block = Block::default() - .borders(Borders::ALL) - .padding(Padding::horizontal(2)) - .style(Style::default()); - let title = Paragraph::new(Text::styled( - "Saved values | Use them in your input", - Style::default().fg(Color::LightYellow), - )) - .block(title_block); - f.render_widget(title, value_chunks[0]); - } - let list_items = app - .interpreter - .vars() - .iter() - .fold(vec![], |mut acc, (i, val)| { + let title_block = Block::default() + .borders(Borders::ALL) + .padding(Padding::horizontal(2)) + .style(Style::default()); + let title = Paragraph::new(Text::styled( + "Saved values/functions", + Style::default().fg(Color::LightYellow), + )) + .block(title_block); + f.render_widget(title, value_chunks[0]); + + let list_items = app.interpreter.env().as_ref().borrow().vars().iter().fold( + vec![], + |mut acc, (i, val)| { let list_item = ListItem::new(Line::from(Span::styled( - format!("{}: {}", i, val), + format!(" {}: {}", i, val), Style::default().fg(Color::LightYellow), ))); acc.push(list_item); acc - }); + }, + ); let output_list = List::new(list_items).block(Block::default().borders(Borders::LEFT)); f.render_widget(output_list, value_chunks[1]); } @@ -125,7 +123,7 @@ pub fn render(app: &mut App, f: &mut Frame) { if let Some(popup) = &app.popup { match popup { - Popup::Funcs => { + Popup::Help => { let popup_layout = Layout::default() .direction(Direction::Vertical) .constraints([ From 805a41859dd1bed3923366d865d79866785cc429 Mon Sep 17 00:00:00 2001 From: Ochir Date: Thu, 4 Jan 2024 23:28:39 +0000 Subject: [PATCH 4/9] Remove Env abstraction. Mostly seems ok. Need to do final scan and add more test before release --- src/app.rs | 2 +- src/interpreter.rs | 82 +++++++++------------------------------------- src/parse.rs | 70 +++++++++++++++++++-------------------- src/ui.rs | 11 ++++--- 4 files changed, 56 insertions(+), 109 deletions(-) diff --git a/src/app.rs b/src/app.rs index 652369e..6a8db42 100644 --- a/src/app.rs +++ b/src/app.rs @@ -75,7 +75,7 @@ impl<'ta> App<'ta> { if !self.expr_history.contains(&expr) { self.expr_history.push(expr); } - if self.expr_selector == self.interpreter.vars_len() { + if self.expr_selector == self.expr_history.len() { self.expr_selector += 1; } // Only reset input if we successfully evaluate diff --git a/src/interpreter.rs b/src/interpreter.rs index 45fccd4..1140760 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -1,11 +1,9 @@ use std::{ - cell::RefCell, collections::HashMap, error::Error, f64::consts::{E, PI}, fmt::Display, ops::Neg, - rc::Rc, }; use crate::{ @@ -16,7 +14,7 @@ use crate::{ #[derive(Debug)] pub struct Interpreter { - env: Rc>, + env: HashMap, } #[derive(Debug, PartialEq)] @@ -33,7 +31,7 @@ pub enum Value { #[derive(Debug, Clone)] pub struct Function { - closure: Rc>, + closure: HashMap, parameters: Vec, arity: usize, body: Expr, @@ -47,13 +45,8 @@ pub enum InterpretError { WrongArity(String, usize, usize), } -#[derive(Debug)] -pub struct Env { - vals: HashMap, -} - impl Function { - fn new(parameters: Vec, body: Expr, closure: Rc>) -> Self { + fn new(parameters: Vec, body: Expr, closure: HashMap) -> Self { Self { arity: parameters.len(), parameters, @@ -71,57 +64,26 @@ impl Function { } } -impl Env { - fn global() -> Self { +impl Interpreter { + pub fn new() -> Self { Self { - // I think we actually don't need this. We would never access things - // outside of the function's closure - vals: Self::default_vars(), + env: Self::default_env(), } } - fn from_vals(vals: HashMap) -> Self { - Self { vals } - } - - fn insert(&mut self, var: String, val: Value) { - self.vals.insert(var, val); - } - - fn get(&self, var: &str) -> Option { - self.vals.get(var).cloned() - } - - fn reset(&mut self) { - self.vals = Self::default_vars() - } - - fn default_vars() -> HashMap { + fn default_env() -> HashMap { HashMap::from_iter([ ("p".to_string(), Value::Num(PI)), ("e".to_string(), Value::Num(E)), ]) - .clone() } - pub fn vars(&self) -> &HashMap { - &self.vals - } -} - -impl Interpreter { - pub fn new() -> Self { - Self { - env: Rc::new(RefCell::new(Env::global())), - } - } - - pub fn with_env(env: Rc>) -> Self { + pub fn with_env(env: HashMap) -> Self { Self { env } } pub fn declare_function(&mut self, name: String, parameters: Vec, body: Expr) { - self.env.as_ref().borrow_mut().insert( + self.env.insert( name, Value::Fn(Function::new(parameters, body, self.env.clone())), ); @@ -150,7 +112,7 @@ impl Interpreter { .interpret_expr(base)? .powf(self.interpret_expr(exponent)?)), Expr::Call(name, args) => { - if let Some(Value::Fn(func)) = self.env.as_ref().borrow().get(name) { + if let Some(Value::Fn(func)) = self.env.get(name) { if args.len() != func.arity { Err(InterpretError::WrongArity( name.to_owned(), @@ -169,10 +131,10 @@ impl Interpreter { } } Expr::Var(var) => { - if let Some(val) = self.env.as_ref().borrow().get(var) { + if let Some(val) = self.env.get(var) { match val { Value::Fn(_) => Err(InterpretError::UnInvokedFunction(var.clone())), - Value::Num(num) => Ok(num), + Value::Num(num) => Ok(*num), } } else { Err(InterpretError::UnknownVariable(var.clone())) @@ -196,26 +158,15 @@ impl Interpreter { } pub fn reset_vars(&mut self) { - self.env.as_ref().borrow_mut().reset(); + self.env = Self::default_env() } pub fn define(&mut self, var: String, val: Value) { - self.env.as_ref().borrow_mut().insert(var, val); - } - - pub fn env(&self) -> Rc> { - self.env.clone() - } - - pub fn vars_len(&self) -> usize { - self.env.as_ref().borrow().vals.len() + self.env.insert(var, val); } - fn new_env(&mut self) { - let outer = self.env.clone(); - self.env = Rc::new(RefCell::new(Env::from_vals( - outer.as_ref().borrow().vals.clone(), - ))); + pub fn env(&self) -> &HashMap { + &self.env } } @@ -250,7 +201,6 @@ impl Display for Value { impl Display for Function { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // TODO: This properly... write!(f, "({}) {}", self.parameters.join(", "), self.body) } } diff --git a/src/parse.rs b/src/parse.rs index 26fb4be..7ad3ff7 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -44,43 +44,9 @@ pub enum Expr { Exponent(Box, Box), Call(String, Vec), Func(Func, Box), - // TODO: Change this to a String Var(String), } -impl Error for ParseErr {} - -impl Display for ParseErr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "ERROR: {}", self.msg) - } -} - -impl Display for Expr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.format()) - } -} - -impl Display for Func { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Func::Sin => SIN, - Func::Sinh => SINH, - Func::Cos => COS, - Func::Cosh => COSH, - Func::Tan => TAN, - Func::Tanh => TANH, - Func::Ln => LN, - Func::Log(base) => return inner_write(format!("log{}", base), f), - } - ) - } -} - impl Expr { pub fn format(&self) -> String { match self { @@ -276,9 +242,6 @@ impl Parser { self.consume(Token::Pipe, "Missing closing pipe")?; return Ok(Expr::Abs(expr)); } - // This is still assuming Ident == Func - // Needs to start treating ( as call expressions properly - // A solution for built in functions might be to treat them as a separate case entirely if let Token::Ident(func) = self.peek() { let func = func.to_owned(); self.advance(); @@ -311,6 +274,39 @@ impl Parser { } } +impl Error for ParseErr {} + +impl Display for ParseErr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ERROR: {}", self.msg) + } +} + +impl Display for Expr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.format()) + } +} + +impl Display for Func { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Func::Sin => SIN, + Func::Sinh => SINH, + Func::Cos => COS, + Func::Cosh => COSH, + Func::Tan => TAN, + Func::Tanh => TANH, + Func::Ln => LN, + Func::Log(base) => return inner_write(format!("log{}", base), f), + } + ) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ui.rs b/src/ui.rs index c636e86..02bbaed 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -42,17 +42,18 @@ pub fn render(app: &mut App, f: &mut Frame) { .block(title_block); f.render_widget(title, value_chunks[0]); - let list_items = app.interpreter.env().as_ref().borrow().vars().iter().fold( - vec![], - |mut acc, (i, val)| { + let list_items = app + .interpreter + .env() + .iter() + .fold(vec![], |mut acc, (i, val)| { let list_item = ListItem::new(Line::from(Span::styled( format!(" {}: {}", i, val), Style::default().fg(Color::LightYellow), ))); acc.push(list_item); acc - }, - ); + }); let output_list = List::new(list_items).block(Block::default().borders(Borders::LEFT)); f.render_widget(output_list, value_chunks[1]); } From f827002d4236b025ab11c3d700d25d7c9a50aa0d Mon Sep 17 00:00:00 2001 From: Ochir Date: Fri, 5 Jan 2024 00:08:03 +0000 Subject: [PATCH 5/9] Updated help notes --- src/app.rs | 1 + src/interpreter.rs | 16 +++++++++ src/lib.rs | 3 ++ src/ui.rs | 85 ++++++++++++++++++++++++++++++---------------- 4 files changed, 76 insertions(+), 29 deletions(-) diff --git a/src/app.rs b/src/app.rs index 6a8db42..cd23248 100644 --- a/src/app.rs +++ b/src/app.rs @@ -19,6 +19,7 @@ pub enum InputType { pub enum Popup { Help, + Function, } pub struct App<'ta> { diff --git a/src/interpreter.rs b/src/interpreter.rs index 1140760..ed02d7b 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -75,6 +75,22 @@ impl Interpreter { HashMap::from_iter([ ("p".to_string(), Value::Num(PI)), ("e".to_string(), Value::Num(E)), + ( + "deg".to_string(), + Value::Fn(Function::new( + vec!["rads".to_string()], + Expr::Binary( + Box::new(Expr::Var("rads".to_string())), + Token::Mult, + Box::new(Expr::Binary( + Box::new(Expr::Num(180.0)), + Token::Div, + Box::new(Expr::Num(PI)), + )), + ), + HashMap::new(), + )), + ), ]) } diff --git a/src/lib.rs b/src/lib.rs index ac9876d..8b49090 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -70,6 +70,9 @@ fn update(app: &mut App, key_event: KeyEvent) { KeyCode::Char('h') if key_event.modifiers.contains(KeyModifiers::CONTROL) => { app.popup = Some(Popup::Help); } + KeyCode::Char('f') if key_event.modifiers.contains(KeyModifiers::CONTROL) => { + app.popup = Some(Popup::Function); + } KeyCode::Char('x') if key_event.modifiers.contains(KeyModifiers::CONTROL) => { app.remove_expr(); } diff --git a/src/ui.rs b/src/ui.rs index 02bbaed..2bc1ac3 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -123,26 +123,37 @@ pub fn render(app: &mut App, f: &mut Frame) { } if let Some(popup) = &app.popup { + let mut render_popup = |message: &str| { + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(15), + Constraint::Percentage(70), + Constraint::Percentage(15), + ]) + .split(f.size()); + + let area = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(10), + Constraint::Percentage(80), + Constraint::Percentage(10), + ]) + .split(popup_layout[1])[1]; + let message_block = Block::default() + .title("Help") + .borders(Borders::ALL) + .padding(Padding::horizontal(3)) + .style(Style::default().fg(Color::White)); + let message_text = Paragraph::new(message) + .wrap(Wrap::default()) + .block(message_block); + f.render_widget(Clear, area); + f.render_widget(message_text, area); + }; match popup { Popup::Help => { - let popup_layout = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Percentage(15), - Constraint::Percentage(70), - Constraint::Percentage(15), - ]) - .split(f.size()); - - let area = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Percentage(10), - Constraint::Percentage(80), - Constraint::Percentage(10), - ]) - .split(popup_layout[1])[1]; - let message = " Available Functions ------------------- @@ -163,16 +174,32 @@ Shortcuts (Ctrl e/v) Reset exprs/vars (Ctrl x) Delete selected expression "; - let message_block = Block::default() - .title("Help") - .borders(Borders::ALL) - .padding(Padding::horizontal(3)) - .style(Style::default().fg(Color::White)); - let message_text = Paragraph::new(message) - .wrap(Wrap::default()) - .block(message_block); - f.render_widget(Clear, area); - f.render_widget(message_text, area); + render_popup(message); + } + Popup::Function => { + let message = " +Custom Functions +---------------- +Defining: fn [NAME]([ARG]..) [BODY] +- NAME: Name of the function +- ARGS: Parameter (comma separated identifier) +- BODY: The expression + +Example: fn myfun(a, b) a + b^2 + +Calling: [NAME]([ARG]...) +- NAME: Name of the function +- ARG: Argument (comma separated expression) + +Examples: +- myfun(10) +- myfun(cos(p)) + +If existing functions / variables are used in a custom function +then a snapshot of them is taken such that even if they are changed +or redefined, the custom function will use the old values +"; + render_popup(message); } } } @@ -186,7 +213,7 @@ Shortcuts let message = if app.popup.is_some() { "(Esc) Back" } else { - "(Esc) Quit | (Ctrl h) Help" + "(Esc) Quit | (Ctrl h) Help | (Ctrl f) Custom fn help" }; let help = Paragraph::new(Text::styled( message, From 3f79a123320223195a9b9a05b826de5b3c9f986f Mon Sep 17 00:00:00 2001 From: Ochir Date: Fri, 5 Jan 2024 00:19:10 +0000 Subject: [PATCH 6/9] Adding some tests --- src/interpreter.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/interpreter.rs b/src/interpreter.rs index ed02d7b..fbaea4c 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -37,7 +37,7 @@ pub struct Function { body: Expr, } -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum InterpretError { UnknownVariable(String), UnknownFunction(String), @@ -220,3 +220,49 @@ impl Display for Function { write!(f, "({}) {}", self.parameters.join(", "), self.body) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn check(expr: Expr, expected: Result) { + let interpreter = Interpreter::new(); + let res = interpreter.interpret_expr(&expr); + assert_eq!(res, expected); + } + + fn check_with_vars( + expr: Expr, + expected: Result, + env: HashMap, + ) { + let interpreter = Interpreter::with_env(env); + let res = interpreter.interpret_expr(&expr); + assert_eq!(res, expected); + } + + #[test] + fn simple_add() { + check( + Expr::Binary( + Box::new(Expr::Num(1.1)), + Token::Plus, + Box::new(Expr::Num(5.0)), + ), + Ok(6.1), + ); + } + + #[test] + fn simple_variable() { + check_with_vars( + Expr::Binary( + Box::new(Expr::Num(12.0)), + Token::Mult, + Box::new(Expr::Var("foo".to_string())), + ), + Ok(144.0), + HashMap::from_iter([("foo".to_string(), Value::Num(12.0))]), + ); + } +} From 5068a1158050426d7944c4c4325f98726b402137 Mon Sep 17 00:00:00 2001 From: Ochir Date: Fri, 5 Jan 2024 00:23:19 +0000 Subject: [PATCH 7/9] Added rads function --- src/interpreter.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/interpreter.rs b/src/interpreter.rs index fbaea4c..c991e43 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -91,6 +91,22 @@ impl Interpreter { HashMap::new(), )), ), + ( + "rads".to_string(), + Value::Fn(Function::new( + vec!["degs".to_string()], + Expr::Binary( + Box::new(Expr::Var("degs".to_string())), + Token::Mult, + Box::new(Expr::Binary( + Box::new(Expr::Num(PI)), + Token::Div, + Box::new(Expr::Num(180.0)), + )), + ), + HashMap::new(), + )), + ), ]) } From 144b1c47cb69688088305b74bb7535089df15241 Mon Sep 17 00:00:00 2001 From: Ochir Date: Fri, 5 Jan 2024 18:13:37 +0000 Subject: [PATCH 8/9] Add tests --- src/app.rs | 58 +++++++++++++++++++++++++- src/interpreter.rs | 34 +++++++++++++-- src/parse.rs | 9 +++- src/token.rs | 101 ++++++++++++++++++++++----------------------- src/ui.rs | 2 +- 5 files changed, 146 insertions(+), 58 deletions(-) diff --git a/src/app.rs b/src/app.rs index cd23248..3b973e7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -63,6 +63,10 @@ impl<'ta> App<'ta> { match self.input_type { InputType::Expr => { let input = &self.input.lines()[0]; + // TODO: Move the tokenizer into the parser so that we're not doing + // this unnecessary allocation. Figure out how to handle end of expressions + // without the use of semicolons (or implicitly add it in but then if someone + // enters one it would terminate their expression which is weird) let mut tokens = Tokenizer::new(input.chars().collect::>().as_slice()) .collect::>(); tokens.push(Token::Eoe); @@ -81,7 +85,7 @@ impl<'ta> App<'ta> { } // Only reset input if we successfully evaluate self.input = textarea(None, None, None); - self.interpreter.define("q".to_string(), Value::Num(val)); + self.interpreter.define("ans".to_string(), Value::Num(val)); self.output = Some(Ok(val)); } Err(err) => self.output = Some(Err(Box::new(err))), @@ -103,12 +107,19 @@ impl<'ta> App<'ta> { Token::Ident(name), "Variable name cannot be empty", )))); + self.input = textarea(None, None, None); + self.input_type = InputType::Expr; } else if !name.chars().all(|c| c.is_ascii_alphabetic()) { self.output = Some(Err(Box::new(ParseErr::new( Token::Ident(name), "Variable names must be letters", )))); + self.input = textarea(None, None, None); + self.input_type = InputType::Expr; } else { + // We know that this is a safe unwrap because the above two cases + // would reset and we can't enter this mode unless we have a + // valid output let output = self.output.as_ref().unwrap().as_ref().unwrap(); self.interpreter.define(name, Value::Num(*output)); self.input = textarea(None, None, None); @@ -137,6 +148,7 @@ impl<'ta> App<'ta> { self.input = textarea(Some(string), None, None); } + // Set the input mode to typing to saving the output as a variable pub fn save_result_input(&mut self) { self.input = textarea( None, @@ -185,3 +197,47 @@ fn textarea<'a>( textarea.move_cursor(tui_textarea::CursorMove::End); textarea } + +#[cfg(test)] +mod tests { + use super::*; + + fn input_and_evaluate(app: &mut App, input: &str) { + app.input = textarea(Some(input.to_string()), None, None); + app.eval(); + } + + fn assert_output(app: &App, expected: f64) { + // Yuck. + if let Some(ref output) = app.output { + assert_eq!(*output.as_ref().unwrap(), expected); + } else { + panic!("Not equal"); + } + } + + #[test] + fn create_and_call_function() { + let mut app = App::new(); + input_and_evaluate(&mut app, "fn foo(x, y) x + y"); + input_and_evaluate(&mut app, "foo (1, 2)"); + assert!(app.output.is_some_and(|r| r.is_ok_and(|n| n == 3.0))); + } + + #[test] + fn save_variable() { + let mut app = App::new(); + input_and_evaluate(&mut app, "12"); + app.save_result_input(); + input_and_evaluate(&mut app, "foo"); + input_and_evaluate(&mut app, "foo * 12"); + assert_output(&app, 144.0); + } + + #[test] + fn test_empty_input() { + let mut app = App::new(); + input_and_evaluate(&mut app, ""); + assert!(app.output.is_some_and(|o| o.is_err())); + } +} diff --git a/src/interpreter.rs b/src/interpreter.rs index c991e43..dc8d915 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -73,7 +73,7 @@ impl Interpreter { fn default_env() -> HashMap { HashMap::from_iter([ - ("p".to_string(), Value::Num(PI)), + ("pi".to_string(), Value::Num(PI)), ("e".to_string(), Value::Num(E)), ( "deg".to_string(), @@ -210,9 +210,9 @@ impl Display for InterpretError { f, "{}", match self { - Self::UnknownVariable(c) => format!("Unknown variable {}", c), - Self::UnknownFunction(c) => format!("Unknown function {}", c), - Self::UnInvokedFunction(c) => format!("Uninvoked function {}", c), + Self::UnknownVariable(v) => format!("Unknown variable {}", v), + Self::UnknownFunction(f) => format!("Unknown function {}", f), + Self::UnInvokedFunction(f) => format!("Uninvoked function {}", f), Self::WrongArity(name, actual, expected) => format!( "Function {} takes {} arguments but {} were provided", name, expected, actual @@ -281,4 +281,30 @@ mod tests { HashMap::from_iter([("foo".to_string(), Value::Num(12.0))]), ); } + + #[test] + fn function_with_closure() { + check_with_vars( + Expr::Call("foo".to_string(), vec![Expr::Var("bar".to_string())]), + Ok(27.0), + HashMap::from_iter([ + ( + "foo".to_string(), + Value::Fn(Function::new( + vec!["x".to_string()], + Expr::Binary( + Box::new(Expr::Exponent( + Box::new(Expr::Var("x".to_string())), + Box::new(Expr::Num(2.0)), + )), + Token::Plus, + Box::new(Expr::Var("bar".to_string())), + ), + HashMap::from_iter([("bar".to_string(), Value::Num(2.0))]), + )), + ), + ("bar".to_string(), Value::Num(5.0)), // The function uses the closure value + ]), + ); + } } diff --git a/src/parse.rs b/src/parse.rs index 7ad3ff7..ce86217 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -22,6 +22,7 @@ pub struct ParseErr { pub msg: &'static str, } +// "Built in" functions, separate of user defined functions #[derive(Debug, PartialEq, Clone)] pub enum Func { Sin, @@ -122,9 +123,15 @@ impl Parser { impl Parser { pub fn parse(&mut self) -> Result { - match self.peek() { + let res = match self.peek() { Token::Fn => self.function(), _ => Ok(Stmt::Expr(self.expression()?)), + }; + if self.at_end() { + res + } else { + // A complete expression was parsed but there were more tokens + Err(ParseErr::new(self.peek().clone(), "Unexpected token")) } } diff --git a/src/token.rs b/src/token.rs index 433e891..2c94416 100644 --- a/src/token.rs +++ b/src/token.rs @@ -39,6 +39,7 @@ impl std::fmt::Display for Token { } } +#[derive(Debug)] pub struct Tokenizer<'a> { input: &'a [char], index: usize, @@ -54,24 +55,35 @@ impl<'a> Iterator for Tokenizer<'a> { type Item = Token; fn next(&mut self) -> Option { - if let Some(next) = self.input.get(self.index) { - self.index += 1; - let token = match next { - ' ' => { - return self.next(); + let next = self.input.get(self.index)?; + self.index += 1; + Some(match next { + ' ' => { + return self.next(); + } + ',' => Token::Comma, + '|' => Token::Pipe, + '/' => Token::Div, + '+' => Token::Plus, + '-' => Token::Minus, + '%' => Token::Mod, + '*' => Token::Mult, + '(' => Token::LParen, + ')' => Token::RParen, + '^' => Token::Power, + '0'..='9' => { + let mut num = next.to_string(); + while self + .input + .get(self.index) + .is_some_and(|c| c.is_ascii_digit()) + { + num.push(self.input[self.index]); + self.index += 1; } - ',' => Token::Comma, - '|' => Token::Pipe, - '/' => Token::Div, - '+' => Token::Plus, - '-' => Token::Minus, - '%' => Token::Mod, - '*' => Token::Mult, - '(' => Token::LParen, - ')' => Token::RParen, - '^' => Token::Power, - '0'..='9' => { - let mut num = next.to_string(); + if self.input.get(self.index).is_some_and(|c| *c == '.') { + num.push(self.input[self.index]); + self.index += 1; while self .input .get(self.index) @@ -80,42 +92,29 @@ impl<'a> Iterator for Tokenizer<'a> { num.push(self.input[self.index]); self.index += 1; } - if self.input.get(self.index).is_some_and(|c| *c == '.') { - num.push(self.input[self.index]); - self.index += 1; - while self - .input - .get(self.index) - .is_some_and(|c| c.is_ascii_digit()) - { - num.push(self.input[self.index]); - self.index += 1; - } - } - Token::Num(num.parse().unwrap()) } - 'A'..='Z' | 'a'..='z' => { - let mut func = next.to_string(); - while self - .input - .get(self.index) - .is_some_and(|c| c.is_ascii_alphabetic()) - { - func.push(self.input[self.index]); - self.index += 1; - } - if func == "fn" { - Token::Fn - } else { - Token::Ident(func) - } + Token::Num(num.parse().unwrap()) + } + 'A'..='Z' | 'a'..='z' => { + let mut func = next.to_string(); + while self + .input + .get(self.index) + .is_some_and(|c| c.is_ascii_alphabetic()) + { + func.push(self.input[self.index]); + self.index += 1; } - _ => return None, // Handle wrong tokens a different way - }; - Some(token) - } else { - None - } + if func == "fn" { + Token::Fn + } else { + Token::Ident(func) + } + } + _ => return None, // Unknown chars just end the parsing. Not sure if good or + // bad... In any case it's probably a bit confusing for the user. Definitely + // confused me lol. + }) } } diff --git a/src/ui.rs b/src/ui.rs index 2bc1ac3..944854a 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -165,7 +165,7 @@ sin(_rads_) sinh(_rads_) tan(_rads_) tanh(_rads_) log_base_(_arg_) ln(_arg_) -The result of a successful eval is stored in q +The result of a successful eval is stored in \"ans\" Shortcuts --------- From 66b7fb046e01c27a918c37bf89b272d88c4b19a8 Mon Sep 17 00:00:00 2001 From: Ochir Date: Fri, 5 Jan 2024 18:15:11 +0000 Subject: [PATCH 9/9] Bump minor version --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6929b8b..671f1d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -331,7 +331,7 @@ dependencies = [ [[package]] name = "qcalc" -version = "0.1.3" +version = "0.2.0" dependencies = [ "color-eyre", "crossterm", diff --git a/Cargo.toml b/Cargo.toml index 131f226..219fad3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "qcalc" -version = "0.1.3" +version = "0.2.0" edition = "2021" authors = ["ochir "] description = """