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

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

            
97
#[cfg(test)]
98
pub(crate) mod test {
99
    #![allow(clippy::cognitive_complexity)]
100

            
101
1
    decl_keyword! {
102
1
        pub(crate) Fruit {
103
1
            "apple" => APPLE,
104
1
            "orange" => ORANGE,
105
1
            "lemon" => LEMON,
106
1
            "guava" => GUAVA,
107
1
            "cherry" | "plum" => STONEFRUIT,
108
1
            annotation "@tasty" => ANN_TASTY,
109
1
        }
110
1
    }
111

            
112
1
    #[test]
113
1
    fn kwd() {
114
1
        use crate::parse::keyword::Keyword;
115
1
        use Fruit::*;
116
1
        assert_eq!(Fruit::from_str("lemon"), LEMON);
117
1
        assert_eq!(Fruit::from_str("cherry"), STONEFRUIT);
118
1
        assert_eq!(Fruit::from_str("plum"), STONEFRUIT);
119
1
        assert_eq!(Fruit::from_str("pear"), UNRECOGNIZED);
120
1
        assert_eq!(Fruit::from_str("@tasty"), ANN_TASTY);
121
1
        assert_eq!(Fruit::from_str("@tastier"), ANN_UNRECOGNIZED);
122

            
123
1
        assert_eq!(APPLE.idx(), 0);
124
1
        assert_eq!(ORANGE.idx(), 1);
125
1
        assert_eq!(ANN_UNRECOGNIZED.idx(), 7);
126
1
        assert_eq!(Fruit::n_vals(), 8);
127

            
128
1
        assert_eq!(Fruit::from_idx(0), Some(APPLE));
129
1
        assert_eq!(Fruit::from_idx(7), Some(ANN_UNRECOGNIZED));
130
1
        assert_eq!(Fruit::from_idx(8), None);
131

            
132
1
        assert_eq!(Fruit::idx_to_str(3), "guava");
133
1
        assert_eq!(Fruit::idx_to_str(999), "<out of range>");
134

            
135
1
        assert_eq!(APPLE.to_str(), "apple");
136
1
        assert_eq!(GUAVA.to_str(), "guava");
137
1
        assert_eq!(ANN_TASTY.to_str(), "@tasty");
138
1
        assert_eq!(STONEFRUIT.to_str(), "cherry/plum");
139
1
        assert_eq!(UNRECOGNIZED.to_str(), "<unrecognized>");
140
1
        assert_eq!(ANN_UNRECOGNIZED.to_str(), "<unrecognized annotation>");
141

            
142
1
        assert!(!GUAVA.is_annotation());
143
1
        assert!(!STONEFRUIT.is_annotation());
144
1
        assert!(!UNRECOGNIZED.is_annotation());
145
1
        assert!(ANN_TASTY.is_annotation());
146
1
        assert!(ANN_UNRECOGNIZED.is_annotation());
147
1
    }
148
}