diff --git a/Cargo.lock b/Cargo.lock index 8dfaca8..a4e332e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,6 +153,14 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "macros" +version = "0.0.0" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "memchr" version = "2.3.4" @@ -186,6 +194,24 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +[[package]] +name = "proc-macro2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + [[package]] name = "radix_trie" version = "0.2.1" @@ -263,6 +289,7 @@ dependencies = [ "dirs", "itertools", "lazy_static", + "macros", "rand", "rustyline", ] @@ -302,6 +329,17 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" +[[package]] +name = "syn" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad184cc9470f9117b2ac6817bfe297307418819ba40552f9b3846f05c33d5373" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "unicode-segmentation" version = "1.7.1" @@ -314,6 +352,12 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + [[package]] name = "utf8parse" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index b7655e6..22065ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,9 @@ license = "MIT" [dependencies] +# Proc macros +macros = { path = "./macros" } + # For colored terminal output colored = "2" diff --git a/macros/Cargo.toml b/macros/Cargo.toml new file mode 100644 index 0000000..8d8106c --- /dev/null +++ b/macros/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "macros" +version = "0.0.0" +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +quote = "1.0.9" +syn = "1.0.71" diff --git a/macros/src/lib.rs b/macros/src/lib.rs new file mode 100644 index 0000000..a49630c --- /dev/null +++ b/macros/src/lib.rs @@ -0,0 +1,104 @@ +use proc_macro::TokenStream; +use syn::{Data, DeriveInput, Type, spanned::Spanned}; +use quote::{quote, quote_spanned, format_ident}; + +macro_rules! error { + ($span:expr, $msg:expr) => { + syn::Error::new($span, $msg).to_compile_error().into() + }; +} + +enum ImplType { + Iter, + Single +} + +// Returns impl type and the inner type +fn kind(ty: &Type) -> Result<(ImplType, &Type), TokenStream> { + Ok(match ty { + Type::Slice(ts) => { + (ImplType::Iter, ts.elem.as_ref()) + } + Type::Array(ta) => { + (ImplType::Iter, ta.elem.as_ref()) + } + Type::Reference(tr) => { + // Recurse! + kind(tr.elem.as_ref())? + } + Type::Path(_) => { + (ImplType::Single, ty) + } + _ => return Err(error!(ty.span(), "Representation type is not a regular type, array, nor slice")) + }) +} + +#[proc_macro_derive(Searchable, attributes(representation))] +pub fn searchable_derive(ts: TokenStream) -> TokenStream { + let ast = syn::parse(ts).unwrap(); + searchable_impl(&ast) +} + +fn searchable_impl(ast: &DeriveInput) -> TokenStream { + + // Find tagged field + let field = match &ast.data { + Data::Struct(struc) => { + struc.fields.iter().find(|fields| { + fields.attrs.iter().any(|attr| attr.path.is_ident("representation")) + }) + }, + _ => return error!(ast.span(), "`Searchable` can only derive structs.") + }; + + let field = if let Some(field) = field { + field + } else { + return error!(ast.span(), "No `#[representable]` annotated member.") + }; + + let ty_span = field.ty.span(); + + // Determine impl type and get inner type of representation + let (kind, ty) = match kind(&field.ty) { + Ok(a) => a, + Err(ts) => return ts + }; + + let name = &ast.ident; + + // Enforce that the representation type has to be `AsRef` + let assert_ident = format_ident!("{}AssertAsRefStr", name); + let assert_as_ref_str = quote_spanned! {ty_span=> + struct #assert_ident where #ty: AsRef; + }; + + let ident = field.ident.as_ref().unwrap(); + let impl_ = match kind { + ImplType::Iter => quote! { + self.#ident + .iter() + .find(|repr| search.to_lowercase().starts_with(&repr.to_lowercase())) + .map(|repr| (self, repr.len())) + }, + ImplType::Single => quote! { + if search.starts_with(&self.#ident) { + Some((self, self.#ident.len())) + } else { + None + } + } + }; + + let gen = quote! { + #[allow(dead_code)] + #assert_as_ref_str + impl Searchable for #name { + fn search<'a>(&'a self, search: &str) -> Option<(&'a Self, usize)> { + #impl_ + } + } + }; + + gen.into() +} \ No newline at end of file diff --git a/src/lib/eval.rs b/src/lib/eval.rs index 4088a3b..9eaeda7 100644 --- a/src/lib/eval.rs +++ b/src/lib/eval.rs @@ -1,4 +1,4 @@ -use super::{constants::Constant, errors::Error, operators::Operator, tokens::Token}; +use super::model::{constants::Constant, errors::Error, operators::Operator, tokens::Token}; pub fn eval(tokens: &[Token]) -> Result { // We need a mutable copy of the tokens diff --git a/src/lib/mod.rs b/src/lib/mod.rs index a840f4f..d7b0e11 100644 --- a/src/lib/mod.rs +++ b/src/lib/mod.rs @@ -1,20 +1,16 @@ -pub mod constants; -pub mod errors; mod eval; -pub mod operators; -mod representable; mod rpn; mod tokenize; -pub mod tokens; + pub mod utils; -pub mod variables; -use errors::Error; +pub mod model; + use eval::eval; use rpn::rpn; use tokenize::tokenize; -use tokens::Token; -use variables::Variable; + +use self::model::{errors::Error, tokens::Token, variables::Variable}; pub fn doeval<'a>(string: &str, vars: &'a [Variable]) -> Result<(f64, Vec>), Error> { let tokens = tokenize(string, vars)?; diff --git a/src/lib/constants.rs b/src/lib/model/constants.rs similarity index 81% rename from src/lib/constants.rs rename to src/lib/model/constants.rs index 900db8f..ec944e0 100644 --- a/src/lib/constants.rs +++ b/src/lib/model/constants.rs @@ -1,6 +1,9 @@ #![allow(clippy::non_ascii_literal)] -use super::representable::{get_by_repr, Representable}; +use super::searchable::Searchable; +use macros::Searchable; + +use super::searchable::get_by_repr; #[allow(clippy::upper_case_acronyms)] #[derive(PartialEq, Clone, Copy, Debug)] @@ -10,18 +13,15 @@ pub enum ConstantType { Tau, } +#[derive(Searchable)] pub struct Constant { pub kind: ConstantType, + + #[representation] pub repr: &'static [&'static str], pub value: f64, } -impl Representable for Constant { - fn repr(&self) -> &[&str] { - self.repr - } -} - static CONSTANTS: &[Constant] = &[ Constant { kind: ConstantType::PI, @@ -44,7 +44,7 @@ impl Constant { pub fn by_type(kind: ConstantType) -> &'static Self { CONSTANTS.iter().find(|c| c.kind == kind).unwrap() } - pub fn by_repr(repr: &str) -> Option<(&'static Self, usize)> { + pub fn by_repr(repr: &str) -> Option<(&Self, usize)> { get_by_repr(repr, CONSTANTS) } pub fn is(repr: &str) -> bool { diff --git a/src/lib/errors.rs b/src/lib/model/errors.rs similarity index 100% rename from src/lib/errors.rs rename to src/lib/model/errors.rs diff --git a/src/lib/model/mod.rs b/src/lib/model/mod.rs new file mode 100644 index 0000000..c3cb8b5 --- /dev/null +++ b/src/lib/model/mod.rs @@ -0,0 +1,6 @@ +pub mod constants; +pub mod errors; +pub mod operators; +mod searchable; +pub mod tokens; +pub mod variables; diff --git a/src/lib/operators.rs b/src/lib/model/operators.rs similarity index 92% rename from src/lib/operators.rs rename to src/lib/model/operators.rs index 28c37a5..e9c51a0 100644 --- a/src/lib/operators.rs +++ b/src/lib/model/operators.rs @@ -2,7 +2,10 @@ use rand::Rng; -use super::representable::{get_by_repr, Representable}; +use super::searchable::Searchable; +use macros::Searchable; + +use super::searchable::get_by_repr; #[derive(PartialEq, Clone, Copy, Debug)] pub enum OperatorType { @@ -27,21 +30,17 @@ pub enum OperatorType { const UNARY_OPERATORS: &[OperatorType] = &[OperatorType::Positive, OperatorType::Negative]; -impl Representable for OperatorType { - fn repr(&self) -> &'static [&'static str] { - Operator::by_type(*self).repr - } -} - #[derive(Clone, PartialEq, Copy)] pub enum Associativity { Left, Right, } -#[derive(Clone, Copy)] +#[derive(Searchable)] pub struct Operator { pub kind: OperatorType, + + #[representation] pub repr: &'static [&'static str], pub precedence: u8, pub associativity: Associativity, @@ -49,24 +48,19 @@ pub struct Operator { pub doit: fn(&[f64]) -> f64, } -impl Representable for Operator { - fn repr(&self) -> &[&str] { - self.repr - } -} - impl Operator { pub fn by_type(kind: OperatorType) -> &'static Self { OPERATORS.iter().find(|op| op.kind == kind).unwrap() } - pub fn by_repr(repr: &str) -> Option<(&'static Self, usize)> { + pub fn by_repr(repr: &str) -> Option<(&Self, usize)> { get_by_repr(repr, OPERATORS) } pub fn is(repr: &str) -> bool { Self::by_repr(repr).is_some() } pub fn unary(repr: &str) -> Option<(&OperatorType, usize)> { - get_by_repr(repr, UNARY_OPERATORS) + let ops = UNARY_OPERATORS.iter().map(|&uop| Self::by_type(uop)); + get_by_repr(repr, ops).map(|(op, idx)| (&op.kind, idx)) } } diff --git a/src/lib/model/searchable.rs b/src/lib/model/searchable.rs new file mode 100644 index 0000000..2388ca8 --- /dev/null +++ b/src/lib/model/searchable.rs @@ -0,0 +1,11 @@ +pub trait Searchable { + fn search<'a>(&'a self, search: &str) -> Option<(&'a Self, usize)>; +} + +pub(super) fn get_by_repr<'a, T, L>(search: &str, list: L) -> Option<(&'a T, usize)> +where + T: Searchable, + L: IntoIterator, +{ + list.into_iter().find_map(|t| t.search(search)) +} diff --git a/src/lib/tokens.rs b/src/lib/model/tokens.rs similarity index 97% rename from src/lib/tokens.rs rename to src/lib/model/tokens.rs index ee89ad4..73c6d64 100644 --- a/src/lib/tokens.rs +++ b/src/lib/model/tokens.rs @@ -66,7 +66,7 @@ impl Token<'_> { ParenType::Right => ')'.to_string(), }, Self::Constant { kind } => Constant::by_type(*kind).repr[0].to_string(), - Self::Variable { inner } => format!("${}", inner.repr), + Self::Variable { inner } => format!("${}", inner.name), } } } diff --git a/src/lib/variables.rs b/src/lib/model/variables.rs similarity index 65% rename from src/lib/variables.rs rename to src/lib/model/variables.rs index 3021950..9bcdc39 100644 --- a/src/lib/variables.rs +++ b/src/lib/model/variables.rs @@ -1,23 +1,15 @@ -use super::representable::{get_by_repr, Searchable}; +use macros::Searchable; -#[derive(Clone, Debug, PartialEq)] +use super::searchable::{get_by_repr, Searchable}; + +#[derive(Searchable, Clone, Debug, PartialEq)] /// Represents a user definable variable pub struct Variable { - pub repr: String, + #[representation] + pub name: String, pub value: f64, } -impl Searchable for Variable { - fn search<'a>(&'a self, search: &str) -> Option<(&'a Self, usize)> { - // Case sensitive - if search.starts_with(&self.repr) { - Some((self, self.repr.len())) - } else { - None - } - } -} - impl Variable { /// Searches for the first variable in `vars` that matches the representation given by the start of `text` /// * `text` - The string to search. Must start with the name of a variable (not a '$') but can diff --git a/src/lib/representable.rs b/src/lib/representable.rs deleted file mode 100644 index 31e60fa..0000000 --- a/src/lib/representable.rs +++ /dev/null @@ -1,23 +0,0 @@ -pub trait Representable { - fn repr(&self) -> &[&str]; -} - -pub trait Searchable { - fn search<'a>(&'a self, search: &str) -> Option<(&'a Self, usize)>; -} - -impl Searchable for Repr { - fn search<'a>(&'a self, search: &str) -> Option<(&'a Self, usize)> { - self.repr() - .iter() - .find(|repr| search.to_lowercase().starts_with(&repr.to_lowercase())) - .map(|repr| (self, repr.len())) - } -} - -pub(super) fn get_by_repr<'a, T: Searchable>( - search: &str, - list: &'a [T], -) -> Option<(&'a T, usize)> { - list.iter().find_map(|t| t.search(search)) -} diff --git a/src/lib/rpn.rs b/src/lib/rpn.rs index 027c88b..380bcdb 100644 --- a/src/lib/rpn.rs +++ b/src/lib/rpn.rs @@ -1,4 +1,4 @@ -use super::{ +use super::model::{ errors::Error, operators::{Associativity, Operator}, tokens::{ParenType, Token}, diff --git a/src/lib/tokenize.rs b/src/lib/tokenize.rs index 9e56c1a..c7e0be1 100644 --- a/src/lib/tokenize.rs +++ b/src/lib/tokenize.rs @@ -1,6 +1,9 @@ use super::{ - constants::Constant, errors::Error, operators::*, tokens::ParenType, tokens::Token, utils, - variables::Variable, + model::{ + constants::Constant, errors::Error, operators::*, tokens::ParenType, tokens::Token, + variables::Variable, + }, + utils, }; #[derive(Clone, Debug, PartialEq)] diff --git a/src/main.rs b/src/main.rs index 24c9d90..734edc4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,11 +13,16 @@ mod lib; use colored::*; use lazy_static::lazy_static; -use lib::doeval; -use lib::errors::Error; -use lib::operators::*; -use lib::tokens::*; -use lib::variables::Variable; +use lib::model::errors::Error; +use lib::{ + doeval, + model::{ + operators::{Associativity, Operator, OperatorType}, + tokens::{ParenType, Token}, + }, +}; + +use lib::model::variables::Variable; use lib::utils; use rustyline::Editor; @@ -160,7 +165,7 @@ fn format_vars(vars: &[Variable]) -> String { .map(|var| { format!( "[ ${} => {} ]", - var.repr.green().bold(), + var.name.green().bold(), format!("{:.3}", var.value).blue() ) }) @@ -206,7 +211,7 @@ fn assign_var_command(input: &str, vars: &mut Vec) -> Result, repr: &str, value: f64) { - let cmp = |var: &Variable| repr.cmp(&var.repr); + let cmp = |var: &Variable| repr.cmp(&var.name); let search = vars.binary_search_by(cmp); match search { Ok(idx) => { @@ -214,7 +219,7 @@ fn assign_var(vars: &mut Vec, repr: &str, value: f64) { } Err(idx) => { let var = Variable { - repr: repr.to_string(), + name: repr.to_string(), value, }; vars.insert(idx, var); @@ -458,11 +463,11 @@ mod tests { )] use crate::{ - lib::constants::*, lib::doeval, - lib::errors::Error, - lib::operators::*, - lib::{tokens::*, variables::Variable}, + lib::model::constants::*, + lib::model::errors::Error, + lib::model::operators::*, + lib::model::{tokens::*, variables::Variable}, stringify, }; @@ -698,11 +703,11 @@ mod tests { fn test_vars() { let test_vars = vec![ Variable { - repr: String::from('v'), + name: String::from('v'), value: 5.0, }, Variable { - repr: String::from("pi"), + name: String::from("pi"), value: 7.0, }, ];