From 5e18e89af498e0def7b8cc3314e8796d8a536766 Mon Sep 17 00:00:00 2001 From: Charlotte Meyer Date: Sat, 5 Nov 2022 12:28:55 +0000 Subject: [PATCH] feat(builtins): add basic math builtins This adds basic addition, substraction, multiplication, and division commands as builtins. All of those operate solely on f64's. Signed-off-by: Charlotte Meyer --- crates/oyster_builtin_proc/src/lib.rs | 5 +- crates/oyster_runtime/src/builtins/math.rs | 57 ++++++ crates/oyster_runtime/src/builtins/mod.rs | 6 + .../oyster_runtime/tests/it/builtins/math.rs | 175 ++++++++++++++++++ .../oyster_runtime/tests/it/builtins/mod.rs | 2 + 5 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 crates/oyster_runtime/src/builtins/math.rs create mode 100644 crates/oyster_runtime/tests/it/builtins/math.rs diff --git a/crates/oyster_builtin_proc/src/lib.rs b/crates/oyster_builtin_proc/src/lib.rs index 75a41ac..baec598 100644 --- a/crates/oyster_builtin_proc/src/lib.rs +++ b/crates/oyster_builtin_proc/src/lib.rs @@ -7,6 +7,7 @@ use syn::{parse_macro_input, AttributeArgs, Ident, ItemFn, Signature}; #[derive(FromMeta)] struct AttrArgs { + name: Option, #[darling(default)] description: String, #[darling(default)] @@ -26,8 +27,8 @@ pub fn builtin(attr: TokenStream, item: TokenStream) -> TokenStream { .. } = sig; - let name = ident.to_string(); let AttrArgs { + name, description, nofork, } = match AttrArgs::from_list(&attrs) { @@ -35,6 +36,8 @@ pub fn builtin(attr: TokenStream, item: TokenStream) -> TokenStream { Err(err) => return err.write_errors().into(), }; + let name = name.unwrap_or_else(|| ident.to_string()); + let builtin = match crate_name("oyster_runtime").expect("oyster_runtime missing") { FoundCrate::Itself => quote! { crate::builtins::Builtin }, FoundCrate::Name(name) => { diff --git a/crates/oyster_runtime/src/builtins/math.rs b/crates/oyster_runtime/src/builtins/math.rs new file mode 100644 index 0000000..e33f0c9 --- /dev/null +++ b/crates/oyster_runtime/src/builtins/math.rs @@ -0,0 +1,57 @@ +use std::{ + borrow::Cow, + ffi::OsStr, + io::{self, Write}, + process, +}; + +use oyster_builtin_proc::builtin; + +use crate::Shell; + +macro_rules! operator { + ($name:literal, $desc:literal, $funcname:ident, $operator:tt, $neutral:literal) => { + #[builtin(name = $name, description = $desc)] + pub fn $funcname(_shell: &mut Shell, args: &[Cow]) { + let mut errored = false; + + let mut parse = |n: &Cow| { + n + .to_str() + .map(|n| { + n.parse::().unwrap_or_else(|_| { + writeln!(io::stderr(), "not a number: {:?}", n).unwrap(); + errored = true; + $neutral + }) + }) + .unwrap_or_else(|| { + writeln!(io::stderr(), "not a number: {:?}", n).unwrap(); + errored = true; + $neutral + }) + }; + + let mut args = args.iter(); + let mut acc = args.next().map(&mut parse).unwrap_or($neutral); + + #[allow(clippy::assign_op_pattern)] + for n in args { + acc = acc $operator parse(n); + } + + writeln!(io::stdout(), "{}", acc).unwrap(); + + process::exit(if errored { + 1 + } else { + 0 + }); + } + } +} + +operator!("+", "adds numbers", add, +, 0.0); +operator!("-", "subtracts numbers", sub, -, 0.0); +operator!("*", "multiplies numbers", mul, *, 1.0); +operator!("/", "divides numbers", div, /, 1.0); diff --git a/crates/oyster_runtime/src/builtins/mod.rs b/crates/oyster_runtime/src/builtins/mod.rs index bac3ad3..795c9be 100644 --- a/crates/oyster_runtime/src/builtins/mod.rs +++ b/crates/oyster_runtime/src/builtins/mod.rs @@ -1,3 +1,5 @@ +pub mod math; + use std::{borrow::Cow, collections::HashMap, ffi::OsStr}; use oyster_builtin_proc::builtin; @@ -55,6 +57,10 @@ impl BuiltinMap { pub fn add_defaults(&mut self) { self.add(exit); self.add(help); + self.add(math::add); + self.add(math::sub); + self.add(math::mul); + self.add(math::div); } } diff --git a/crates/oyster_runtime/tests/it/builtins/math.rs b/crates/oyster_runtime/tests/it/builtins/math.rs new file mode 100644 index 0000000..c731b71 --- /dev/null +++ b/crates/oyster_runtime/tests/it/builtins/math.rs @@ -0,0 +1,175 @@ +use expect_test::{expect, Expect}; +use oyster_parser::ast; +use oyster_runtime::Shell; + +use crate::collect_output; + +fn check(ast: &ast::Code, expect: Expect) { + let actual = collect_output(|| { + let mut shell = Shell::new().unwrap(); + shell.builtins_mut().add_defaults(); + shell.run(ast).unwrap(); + }); + expect.assert_debug_eq(&actual); +} + +#[test] +fn add_single() { + let ast = { + use oyster_parser::ast::*; + + Code(vec![Statement::Pipeline(Pipeline(vec![Command( + vec![ + Word(vec![WordPart::Text("+")]), + Word(vec![WordPart::Text("5")]), + ], + Redirect::None, + )]))]) + }; + + check( + &ast, + expect![[r#" + "5\r\n" + "#]], + ); +} + +#[test] +fn add_multiple() { + let ast = { + use oyster_parser::ast::*; + + Code(vec![Statement::Pipeline(Pipeline(vec![Command( + vec![ + Word(vec![WordPart::Text("+")]), + Word(vec![WordPart::Text("5")]), + Word(vec![WordPart::Text("2")]), + Word(vec![WordPart::Text("3")]), + ], + Redirect::None, + )]))]) + }; + + check( + &ast, + expect![[r#" + "10\r\n" + "#]], + ); +} + +#[test] +fn sub() { + let ast = { + use oyster_parser::ast::*; + + Code(vec![Statement::Pipeline(Pipeline(vec![Command( + vec![ + Word(vec![WordPart::Text("-")]), + Word(vec![WordPart::Text("5")]), + Word(vec![WordPart::Text("2")]), + ], + Redirect::None, + )]))]) + }; + + check( + &ast, + expect![[r#" + "3\r\n" + "#]], + ); +} + +#[test] +fn mul() { + let ast = { + use oyster_parser::ast::*; + + Code(vec![Statement::Pipeline(Pipeline(vec![Command( + vec![ + Word(vec![WordPart::Text("*")]), + Word(vec![WordPart::Text("5")]), + Word(vec![WordPart::Text("2")]), + ], + Redirect::None, + )]))]) + }; + + check( + &ast, + expect![[r#" + "10\r\n" + "#]], + ); +} + +#[test] +fn simple_div() { + let ast = { + use oyster_parser::ast::*; + + Code(vec![Statement::Pipeline(Pipeline(vec![Command( + vec![ + Word(vec![WordPart::Text("/")]), + Word(vec![WordPart::Text("10")]), + Word(vec![WordPart::Text("2")]), + ], + Redirect::None, + )]))]) + }; + + check( + &ast, + expect![[r#" + "5\r\n" + "#]], + ); +} + +#[test] +fn fraction_div() { + let ast = { + use oyster_parser::ast::*; + + Code(vec![Statement::Pipeline(Pipeline(vec![Command( + vec![ + Word(vec![WordPart::Text("/")]), + Word(vec![WordPart::Text("9")]), + Word(vec![WordPart::Text("2")]), + ], + Redirect::None, + )]))]) + }; + + check( + &ast, + expect![[r#" + "4.5\r\n" + "#]], + ); +} + +#[test] +fn div_inf() { + let ast = { + use oyster_parser::ast::*; + + Code(vec![Statement::Pipeline(Pipeline(vec![Command( + vec![ + Word(vec![WordPart::Text("/")]), + Word(vec![WordPart::Text("1")]), + Word(vec![WordPart::Text("0")]), + ], + Redirect::None, + )]))]) + }; + + check( + &ast, + expect![[r#" + "inf\r\n" + "#]], + ); +} diff --git a/crates/oyster_runtime/tests/it/builtins/mod.rs b/crates/oyster_runtime/tests/it/builtins/mod.rs index a68b1ff..a064e79 100644 --- a/crates/oyster_runtime/tests/it/builtins/mod.rs +++ b/crates/oyster_runtime/tests/it/builtins/mod.rs @@ -1,3 +1,5 @@ +mod math; + use std::{borrow::Cow, ffi::OsStr, io::Write}; use expect_test::{expect, Expect};