diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index dbda92c..5f23cb3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -46,10 +46,14 @@ jobs: run: cargo clippy -- -D warnings -W clippy::nursery -W clippy::pedantic - name: Run cargo test - continue-on-error: ${{ matrix.rust == 'nightly' }} run: cargo test + - name: Run cargo test all features + if: ${{ matrix.rust == 'nightly' }} + run: cargo test --all-features + - name: Run cargo doc + if: ${{ matrix.rust == 'nightly' }} run: cargo doc --release --all-features env: RUSTDOCFLAGS: "-Dwarnings" diff --git a/Cargo.toml b/Cargo.toml index ec076e7..6de6673 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,9 +23,17 @@ syn = { version = "2.0", features = ["full", "extra-traits"] } # MIT or Apache-2 proc-macro2 = "1.0" # MIT or Apache-2.0 quote = "1.0" # MIT or Apache-2.0 +[features] +# Nightly only +cfg_attribute = [] + [dev-dependencies] criterion = { version = "0.4.0", features = ["html_reports"] } # MIT or Apache-2.0 [[bench]] name = "match" harness = false + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] diff --git a/README.md b/README.md index d9a8ea4..15f065f 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,26 @@ Furthermore, this implementation uses the compact double-array data structure to achieve efficient state-to-state traversal, and the time complexity becomes *O(m)*. +## `cfg` attribute + +Only when using Nightly Rust, this macro supports conditional compilation with +the `cfg` attribute. To use this feature, enable `features = ["cfg_attribute"]` +in your `Cargo.toml`. + +### Example + +```rust +trie_match! { + match x { + #[cfg(feature = "foo")] + "a" => { .. } + #[cfg(feature = "bar")] + "b" => { .. } + _ => { .. } + } +} +``` + ## Limitations The followings are different from the normal `match` expression: @@ -70,7 +90,6 @@ The followings are different from the normal `match` expression: * The wildcard is evaluated last. (The normal `match` expression does not match patterns after the wildcard.) * Pattern bindings are unavailable. -* Attributes for match arms are unavailable. * Guards are unavailable. Sometimes the normal `match` expression is faster, depending on how diff --git a/src/lib.rs b/src/lib.rs index bb67013..1af78ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg_attr(feature = "cfg_attribute", feature(proc_macro_expand))] + //! # `trie_match! {}` //! //! This macro speeds up Rust's `match` expression for comparing strings by using a compact @@ -13,16 +15,50 @@ //! //! let x = "abd"; //! -//! trie_match! { +//! let result = trie_match! { //! match x { -//! "a" => { println!("x"); } -//! "abc" => { println!("y"); } -//! "abd" | "bcc" => { println!("z"); } -//! "bc" => { println!("w"); } -//! _ => { println!(" "); } +//! "a" => 0, +//! "abc" => 1, +//! "abd" | "bcc" => 2, +//! "bc" => 3, +//! _ => 4, //! } -//! } +//! }; +//! +//! assert_eq!(result, 2); //! ``` +#![cfg_attr( + feature = "cfg_attribute", + doc = r#" +## `cfg` attribute + +Only when using Nightly Rust, this macro supports conditional compilation with +the `cfg` attribute. To use this feature, enable `features = ["cfg_attribute"]` +in your `Cargo.toml`. + +### Example + +``` +use trie_match::trie_match; + +let x = "abd"; + +let result = trie_match! { + match x { + #[cfg(not(feature = "foo"))] + "a" => 0, + "abc" => 1, + #[cfg(feature = "bar")] + "abd" | "bcc" => 2, + "bc" => 3, + _ => 4, + } +}; + +assert_eq!(result, 4); +``` +"# +)] //! //! ## Limitations //! @@ -32,8 +68,8 @@ //! * The wildcard is evaluated last. (The normal `match` expression does not //! match patterns after the wildcard.) //! * Pattern bindings are unavailable. -//! * Attributes for match arms are unavailable. //! * Guards are unavailable. + mod trie; extern crate proc_macro; @@ -47,6 +83,13 @@ use syn::{ PatReference, PatSlice, PatWild, }; +#[cfg(feature = "cfg_attribute")] +use proc_macro2::Ident; +#[cfg(feature = "cfg_attribute")] +use syn::{Attribute, Meta}; + +use crate::trie::Sparse; + static ERROR_UNEXPECTED_PATTERN: &str = "`trie_match` only supports string literals, byte string literals, and u8 slices as patterns"; static ERROR_ATTRIBUTE_NOT_SUPPORTED: &str = "attribute not supported here"; @@ -55,7 +98,13 @@ static ERROR_UNREACHABLE_PATTERN: &str = "unreachable pattern"; static ERROR_PATTERN_NOT_COVERED: &str = "non-exhaustive patterns: `_` not covered"; static ERROR_EXPECTED_U8_LITERAL: &str = "expected `u8` integer literal"; -use crate::trie::Sparse; +#[cfg(not(feature = "cfg_attribute"))] +static ERROR_ATTRIBUTE_NOT_SUPPORTED_CFG: &str = + "attribute not supported here\nnote: consider enabling the `cfg_attribute` feature: \ + https://docs.rs/trie-match/latest/trie_match/#cfg-attribute"; + +#[cfg(feature = "cfg_attribute")] +static ERROR_NOT_CFG_ATTRIBUTE: &str = "only supports the cfg attribute"; /// Converts a literal pattern into a byte sequence. fn convert_literal_pattern(pat: &ExprLit) -> Result>, Error> { @@ -170,6 +219,28 @@ fn retrieve_match_patterns(pat: &Pat) -> Result>>, Error> { Ok(pats) } +#[cfg(feature = "cfg_attribute")] +fn evaluate_cfg_attribute(attrs: &[Attribute]) -> Result { + for attr in attrs { + let ident = attr.path().get_ident().map(Ident::to_string); + if ident.as_deref() == Some("cfg") { + if let Meta::List(list) = &attr.meta { + let tokens = &list.tokens; + let cfg_macro: proc_macro::TokenStream = quote! { cfg!(#tokens) }.into(); + let expr = cfg_macro + .expand_expr() + .map_err(|e| Error::new(tokens.span(), e.to_string()))?; + if expr.to_string() == "false" { + return Ok(false); + } + continue; + } + } + return Err(Error::new(attr.span(), ERROR_NOT_CFG_ATTRIBUTE)); + } + Ok(true) +} + struct MatchInfo { bodies: Vec, pattern_map: HashMap, usize>, @@ -180,20 +251,25 @@ fn parse_match_arms(arms: Vec) -> Result { let mut pattern_map = HashMap::new(); let mut wildcard_idx = None; let mut bodies = vec![]; - for ( - i, - Arm { - attrs, - pat, - guard, - body, - .. - }, - ) in arms.into_iter().enumerate() + let mut i = 0; + #[allow(clippy::explicit_counter_loop)] + for Arm { + attrs, + pat, + guard, + body, + .. + } in arms { + #[cfg(feature = "cfg_attribute")] + if !evaluate_cfg_attribute(&attrs)? { + continue; + } + #[cfg(not(feature = "cfg_attribute"))] if let Some(attr) = attrs.first() { - return Err(Error::new(attr.span(), ERROR_ATTRIBUTE_NOT_SUPPORTED)); + return Err(Error::new(attr.span(), ERROR_ATTRIBUTE_NOT_SUPPORTED_CFG)); } + if let Some((if_token, _)) = guard { return Err(Error::new(if_token.span(), ERROR_GUARD_NOT_SUPPORTED)); } @@ -212,6 +288,7 @@ fn parse_match_arms(arms: Vec) -> Result { } } bodies.push(*body); + i += 1; } let Some(wildcard_idx) = wildcard_idx else { return Err(Error::new(Span::call_site(), ERROR_PATTERN_NOT_COVERED)); diff --git a/tests/tests.rs b/tests/tests.rs index e52cc5e..345fe0a 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -202,3 +202,50 @@ fn test_slice_ref_numbers() { assert_eq!(f(&[0, 1, 2]), 0); assert_eq!(f(&[0, 1]), 1); } + +#[cfg(feature = "cfg_attribute")] +#[test] +fn test_cfg_attribute() { + let f = |text| { + trie_match! { + match text { + #[cfg(test)] + "a" => 0, + #[cfg(not(test))] + "b" => 1, + _ => 2, + } + } + }; + assert_eq!(f("a"), 0); + assert_eq!(f("b"), 2); + assert_eq!(f("c"), 2); +} + +#[cfg(feature = "cfg_attribute")] +#[test] +fn test_cfg_attribute_combination() { + let f = |text| { + trie_match! { + match text { + #[cfg(test)] + #[cfg(feature = "cfg_attribute")] + "a" => 0, + #[cfg(not(test))] + #[cfg(feature = "cfg_attribute")] + "b" => 1, + #[cfg(test)] + #[cfg(not(feature = "cfg_attribute"))] + "c" => 2, + #[cfg(not(test))] + #[cfg(not(feature = "cfg_attribute"))] + "d" => 3, + _ => 4, + } + } + }; + assert_eq!(f("a"), 0); + assert_eq!(f("b"), 4); + assert_eq!(f("c"), 4); + assert_eq!(f("d"), 4); +}