Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support cfg attribute on Nightly Rust #10

Merged
merged 11 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
117 changes: 97 additions & 20 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
//!
Expand All @@ -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;
Expand All @@ -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";
Expand All @@ -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<Option<Vec<u8>>, Error> {
Expand Down Expand Up @@ -170,6 +219,28 @@ fn retrieve_match_patterns(pat: &Pat) -> Result<Vec<Option<Vec<u8>>>, Error> {
Ok(pats)
}

#[cfg(feature = "cfg_attribute")]
fn evaluate_cfg_attribute(attrs: &[Attribute]) -> Result<bool, Error> {
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<Expr>,
pattern_map: HashMap<Vec<u8>, usize>,
Expand All @@ -180,20 +251,25 @@ fn parse_match_arms(arms: Vec<Arm>) -> Result<MatchInfo, Error> {
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));
}
Expand All @@ -212,6 +288,7 @@ fn parse_match_arms(arms: Vec<Arm>) -> Result<MatchInfo, Error> {
}
}
bodies.push(*body);
i += 1;
}
let Some(wildcard_idx) = wildcard_idx else {
return Err(Error::new(Span::call_site(), ERROR_PATTERN_NOT_COVERED));
Expand Down
47 changes: 47 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
vbkaisetsu marked this conversation as resolved.
Show resolved Hide resolved

#[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);
}