Skip to content

Commit

Permalink
Add calc command (#263)
Browse files Browse the repository at this point in the history
* Add calc command

* Parse float

* Refactor parser

* Add tests

* Refactor parser

* Update changelog
  • Loading branch information
vinc authored Nov 1, 2021
1 parent 591a54e commit 8457c0f
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 50 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Changelog

## Unreleased
- Add calc command #263
- Add website (#261)
- Fix VGA issues with real hardware (#258)
- Add rust binaries support (#255)
- Add dynamical disk information (#252)
Expand Down
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ lazy_static = { version = "1.4.0", features = ["spin_no_std"] }
libm = "0.2.1"
linked_list_allocator = "0.9.0"
littlewing = { version = "0.7.0", default-features = false }
nom = { git = "https://github.com/Geal/nom", default-features = false, features = ["alloc"] }
object = { version = "0.26.2", default-features = false, features = ["read"] }
pbkdf2 = { version = "0.9.0", default-features = false }
pc-keyboard = "0.5.1"
Expand Down
182 changes: 182 additions & 0 deletions src/usr/calc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
use crate::usr;
use crate::api::prompt::Prompt;
use crate::api::console::Style;

use alloc::boxed::Box;
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;

use nom::branch::alt;
use nom::character::complete::{char, space0};
use nom::number::complete::float;
use nom::combinator::map;
use nom::multi::many0;
use nom::sequence::{delimited, tuple};
use nom::IResult;

// Adapted from Basic Calculator
// Copyright 2021 Balaji Sivaraman
// https://github.com/balajisivaraman/basic_calculator_rs

#[derive(Debug, PartialEq)]
pub enum Exp {
Num(f32),
Add(Box<Exp>, Box<Exp>),
Sub(Box<Exp>, Box<Exp>),
Mul(Box<Exp>, Box<Exp>),
Div(Box<Exp>, Box<Exp>),
Exp(Box<Exp>, Box<Exp>),
}

// Parser

fn parse(input: &str) -> IResult<&str, Exp> {
let (input, num1) = parse_term(input)?;
let (input, exps) = many0(tuple((alt((char('+'), char('-'))), parse_term)))(input)?;
Ok((input, parse_exp(num1, exps)))
}

fn parse_term(input: &str) -> IResult<&str, Exp> {
let (input, num1) = parse_factor(input)?;
let (input, exps) = many0(tuple((alt((char('/'), char('*'))), parse_factor)))(input)?;
Ok((input, parse_exp(num1, exps)))
}

fn parse_factor(input: &str) -> IResult<&str, Exp> {
let (input, num1) = alt((parse_parens, parse_num))(input)?;
let (input, exps) = many0(tuple((char('^'), parse_factor)))(input)?;
Ok((input, parse_exp(num1, exps)))
}

fn parse_parens(input: &str) -> IResult<&str, Exp> {
delimited(space0, delimited(char('('), parse, char(')')), space0)(input)
}

fn parse_num(input: &str) -> IResult<&str, Exp> {
map(delimited(space0, float, space0), Exp::Num)(input)
}

fn parse_exp(exp: Exp, rem: Vec<(char, Exp)>) -> Exp {
rem.into_iter().fold(exp, |acc, val| parse_op(val, acc))
}

fn parse_op(tup: (char, Exp), exp1: Exp) -> Exp {
let (op, exp2) = tup;
match op {
'+' => Exp::Add(Box::new(exp1), Box::new(exp2)),
'-' => Exp::Sub(Box::new(exp1), Box::new(exp2)),
'*' => Exp::Mul(Box::new(exp1), Box::new(exp2)),
'/' => Exp::Div(Box::new(exp1), Box::new(exp2)),
'^' => Exp::Exp(Box::new(exp1), Box::new(exp2)),
_ => panic!("Unknown operation"),
}
}

// Evaluation

fn eval(exp: Exp) -> f32 {
match exp {
Exp::Num(num) => num,
Exp::Add(exp1, exp2) => eval(*exp1) + eval(*exp2),
Exp::Sub(exp1, exp2) => eval(*exp1) - eval(*exp2),
Exp::Mul(exp1, exp2) => eval(*exp1) * eval(*exp2),
Exp::Div(exp1, exp2) => eval(*exp1) / eval(*exp2),
Exp::Exp(exp1, exp2) => libm::powf(eval(*exp1), eval(*exp2)),
}
}

// REPL

fn parse_eval(line: &str) -> Result<f32, String> {
match parse(&line) {
Ok((line, parsed)) => {
if line.is_empty() {
Ok(eval(parsed))
} else {
Err(format!("Could not parse '{}'", line))
}
},
Err(_) => {
Err(format!("Could not parse '{}'", line))
},
}
}

fn repl() -> usr::shell::ExitCode {
println!("MOROS Calc v0.1.0\n");
let csi_color = Style::color("Cyan");
let csi_error = Style::color("LightRed");
let csi_reset = Style::reset();
let prompt_string = format!("{}>{} ", csi_color, csi_reset);

let mut prompt = Prompt::new();
let history_file = "~/.calc-history";
prompt.history.load(history_file);

while let Some(line) = prompt.input(&prompt_string) {
if line == "exit" || line == "quit" {
break;
}

match parse_eval(&line) {
Ok(res) => {
println!("{}\n", res);
}
Err(msg) => {
println!("{}Error:{} {}\n", csi_error, csi_reset, msg);
continue;
}
}

if !line.is_empty() {
prompt.history.add(&line);
prompt.history.save(history_file);
}
}
usr::shell::ExitCode::CommandSuccessful
}

pub fn main(args: &[&str]) -> usr::shell::ExitCode {
if args.len() == 1 {
repl()
} else {
match parse_eval(&args[1..].join(" ")) {
Ok(res) => {
println!("{}", res);
usr::shell::ExitCode::CommandSuccessful
}
Err(msg) => {
println!("{}", msg);
usr::shell::ExitCode::CommandError
}
}
}
}

#[test_case]
pub fn test_calc() {
macro_rules! eval {
($e:expr) => {
format!("{}", parse_eval($e).unwrap())
};
}

assert_eq!(eval!("1"), "1");
assert_eq!(eval!("1.5"), "1.5");

assert_eq!(eval!("1 + 2"), "3");
assert_eq!(eval!("1 + 2 + 3"), "6");
assert_eq!(eval!("1 + 2.5"), "3.5");
assert_eq!(eval!("1 + 2.5"), "3.5");
assert_eq!(eval!("2 - 1"), "1");
assert_eq!(eval!("1 - 2"), "-1");
assert_eq!(eval!("2 * 3"), "6");
assert_eq!(eval!("2 * 3.5"), "7");
assert_eq!(eval!("6 / 2"), "3");
assert_eq!(eval!("6 / 4"), "1.5");
assert_eq!(eval!("2 ^ 4"), "16");

assert_eq!(eval!("2 * 3 + 4"), "10");
assert_eq!(eval!("2 * (3 + 4)"), "14");
}
Loading

0 comments on commit 8457c0f

Please sign in to comment.