1
//! Declares macros to help implementing parsers.
2

            
3
// https://github.com/rust-lang/rust-clippy/issues/6860
4
#![allow(renamed_and_removed_lints, clippy::unknown_clippy_lints)]
5

            
6
/// Macro for declaring a keyword enumeration to help parse a document.
7
///
8
/// A keyword enumeration implements the Keyword trait.
9
///
10
/// These enums are a bit different from those made by `caret`, in a
11
/// few ways.  Notably, they are optimized for parsing, they are
12
/// required to be compact, and they allow multiple strings to be mapped to
13
/// a single index.
14
///
15
/// ```ignore
16
/// decl_keyword! {
17
///    Location {
18
//         "start" => START,
19
///        "middle" | "center" => MID,
20
///        "end" => END
21
///    }
22
/// }
23
///
24
/// assert_eq!(Location::from_str("start"), Location::START);
25
/// assert_eq!(Location::from_str("stfff"), Location::UNRECOGNIZED);
26
/// ```
27
#[allow(unused_macro_rules)]
28
macro_rules! decl_keyword {
29
    { $(#[$meta:meta])* $v:vis
30
      $name:ident { $( $($anno:ident)? $($s:literal)|+ => $i:ident),* $(,)? } } => {
31
        #[derive(Copy,Clone,Eq,PartialEq,Debug,std::hash::Hash)]
32
        #[allow(non_camel_case_types)]
33
        $(#[$meta])*
34
        #[allow(unknown_lints)]
35
        // https://github.com/rust-lang/rust-clippy/issues/6860
36
        #[allow(renamed_and_removed_lints, clippy::unknown_clippy_lints)]
37
        #[allow(clippy::upper_case_acronyms)]
38
        $v enum $name {
39
            $( $i , )*
40
            UNRECOGNIZED,
41
            ANN_UNRECOGNIZED
42
        }
43
        impl $crate::KeywordEncodable for $name {
44
92026
            fn to_str(self) -> &'static str {
45
                use $name::*;
46
92026
                match self {
47
2
                    $( $i => decl_keyword![@impl join $($s),+], )*
48
4
                    UNRECOGNIZED => "<unrecognized>",
49
2
                    ANN_UNRECOGNIZED => "<unrecognized annotation>"
50
                }
51
92026
            }
52
        }
53
        impl $crate::parse::keyword::Keyword for $name {
54
287401
            fn idx(self) -> usize { self as usize }
55
13880
            fn n_vals() -> usize { ($name::ANN_UNRECOGNIZED as usize) + 1 }
56
13709
            fn unrecognized() -> Self { $name::UNRECOGNIZED }
57
12842
            fn ann_unrecognized() -> Self { $name::ANN_UNRECOGNIZED }
58
83232
            fn from_str(s : &str) -> Self {
59
                // Note usage of phf crate to create a perfect hash over
60
                // the possible keywords.  It will be even better if someday
61
                // the phf crate can find hash functions that are better
62
                // than siphash.
63
                const KEYWORD: phf::Map<&'static str, $name> = phf::phf_map! {
64
                    $( $( $s => $name::$i , )+ )*
65
                };
66
83232
                match KEYWORD.get(s) {
67
80992
                    Some(k) => *k,
68
2240
                    None => if s.starts_with('@') {
69
6
                        $name::ANN_UNRECOGNIZED
70
                    } else {
71
2234
                        $name::UNRECOGNIZED
72
                    }
73
                }
74
83232
            }
75
10
            fn from_idx(i : usize) -> Option<Self> {
76
                // Note looking up the value in a vec.  This may or may
77
                // not be faster than a case statement would be.
78
                static VALS: std::sync::LazyLock<Vec<$name>> =
79
                    std::sync::LazyLock::new(
80
2
                        || vec![ $($name::$i , )*
81
2
                              $name::UNRECOGNIZED,
82
2
                                 $name::ANN_UNRECOGNIZED ]);
83
10
                VALS.get(i).copied()
84
10
            }
85
5887
            fn is_annotation(self) -> bool {
86
                use $name::*;
87
5887
                match self {
88
22
                    $( $i => decl_keyword![@impl is_anno $($anno)? ], )*
89
2
                    UNRECOGNIZED => false,
90
12
                    ANN_UNRECOGNIZED => true,
91
                }
92
5887
            }
93
        }
94
    };
95
    [ @impl is_anno annotation ] => ( true );
96
    [ @impl is_anno $x:ident ] => ( compile_error!("unrecognized keyword; not annotation") );
97
    [ @impl is_anno ] => ( false );
98
    [ @impl join $s:literal ] => ( $s );
99
    [ @impl join $s:literal , $($ss:literal),+ ] => (
100
        concat! { $s, "/", decl_keyword![@impl join $($ss),*] }
101
    );
102
}
103

            
104
#[cfg(test)]
105
pub(crate) mod test {
106
    #![allow(clippy::cognitive_complexity)]
107

            
108
    use crate::KeywordEncodable;
109

            
110
    decl_keyword! {
111
        pub(crate) Fruit {
112
            "apple" => APPLE,
113
            "orange" => ORANGE,
114
            "lemon" => LEMON,
115
            "guava" => GUAVA,
116
            "cherry" | "plum" => STONEFRUIT,
117
            "banana" => BANANA,
118
            annotation "@tasty" => ANN_TASTY,
119
        }
120
    }
121

            
122
    #[test]
123
    fn kwd() {
124
        use crate::parse::keyword::Keyword;
125
        use Fruit::*;
126
        assert_eq!(Fruit::from_str("lemon"), LEMON);
127
        assert_eq!(Fruit::from_str("cherry"), STONEFRUIT);
128
        assert_eq!(Fruit::from_str("plum"), STONEFRUIT);
129
        assert_eq!(Fruit::from_str("pear"), UNRECOGNIZED);
130
        assert_eq!(Fruit::from_str("@tasty"), ANN_TASTY);
131
        assert_eq!(Fruit::from_str("@tastier"), ANN_UNRECOGNIZED);
132

            
133
        assert_eq!(APPLE.idx(), 0);
134
        assert_eq!(ORANGE.idx(), 1);
135
        assert_eq!(ANN_UNRECOGNIZED.idx(), 8);
136
        assert_eq!(Fruit::n_vals(), 9);
137

            
138
        assert_eq!(Fruit::from_idx(0), Some(APPLE));
139
        assert_eq!(Fruit::from_idx(8), Some(ANN_UNRECOGNIZED));
140
        assert_eq!(Fruit::from_idx(9), None);
141

            
142
        assert_eq!(Fruit::idx_to_str(3), "guava");
143
        assert_eq!(Fruit::idx_to_str(999), "<out of range>");
144

            
145
        assert_eq!(APPLE.to_str(), "apple");
146
        assert_eq!(GUAVA.to_str(), "guava");
147
        assert_eq!(ANN_TASTY.to_str(), "@tasty");
148
        assert_eq!(STONEFRUIT.to_str(), "cherry/plum");
149
        assert_eq!(UNRECOGNIZED.to_str(), "<unrecognized>");
150
        assert_eq!(ANN_UNRECOGNIZED.to_str(), "<unrecognized annotation>");
151

            
152
        assert!(!GUAVA.is_annotation());
153
        assert!(!STONEFRUIT.is_annotation());
154
        assert!(!UNRECOGNIZED.is_annotation());
155
        assert!(ANN_TASTY.is_annotation());
156
        assert!(ANN_UNRECOGNIZED.is_annotation());
157
    }
158
}