Skip to main content

pelicode_core/
config.rs

1//! Parameters of feature extraction.
2
3use std::fmt;
4use std::str::FromStr;
5
6use serde::{Deserialize, Serialize};
7
8use crate::Error;
9
10/// Version of the normalization rules. Features computed with different
11/// versions must never be compared with each other.
12pub const NORMALIZER_VERSION: u32 = 1;
13
14/// Normalization level (see specification ยง4.2).
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
16pub enum Level {
17    /// Tokens without whitespace and comments.
18    N0,
19    /// N0 with identifiers and literals abstracted.
20    N1,
21    /// N1 with type names abstracted.
22    N2,
23    /// Sequence of named AST node kinds.
24    N3,
25}
26
27impl Level {
28    /// Every level, from the least to the most abstract.
29    pub const ALL: [Level; 4] = [Level::N0, Level::N1, Level::N2, Level::N3];
30
31    /// The level as a number, 0 for [`Level::N0`] to 3 for [`Level::N3`], as stored in a database.
32    ///
33    /// # Examples
34    ///
35    /// ```
36    /// use pelicode_core::Level;
37    ///
38    /// assert_eq!(Level::N2.as_u8(), 2);
39    /// ```
40    pub fn as_u8(self) -> u8 {
41        self as u8
42    }
43
44    /// The level of a number returned by [`Level::as_u8`]; `None` for any other number.
45    ///
46    /// # Examples
47    ///
48    /// ```
49    /// use pelicode_core::Level;
50    ///
51    /// assert_eq!(Level::from_u8(1), Some(Level::N1));
52    /// assert_eq!(Level::from_u8(4), None);
53    /// ```
54    pub fn from_u8(value: u8) -> Option<Level> {
55        Level::ALL.get(value as usize).copied()
56    }
57}
58
59impl fmt::Display for Level {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        write!(f, "N{}", self.as_u8())
62    }
63}
64
65impl FromStr for Level {
66    type Err = Error;
67
68    fn from_str(s: &str) -> Result<Self, Self::Err> {
69        match s.to_ascii_uppercase().as_str() {
70            "N0" | "0" => Ok(Level::N0),
71            "N1" | "1" => Ok(Level::N1),
72            "N2" | "2" => Ok(Level::N2),
73            "N3" | "3" => Ok(Level::N3),
74            _ => Err(Error::InvalidConfig(format!("unknown level: {s}"))),
75        }
76    }
77}
78
79/// How identifiers are abstracted from N1.
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
81#[serde(rename_all = "lowercase")]
82pub enum IdentMode {
83    /// Every identifier becomes `$ID`.
84    #[default]
85    Uniform,
86    /// Identifiers are numbered by first appearance in the function (`$ID1`, `$ID2`, ...).
87    Indexed,
88}
89
90impl fmt::Display for IdentMode {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        f.write_str(match self {
93            IdentMode::Uniform => "uniform",
94            IdentMode::Indexed => "indexed",
95        })
96    }
97}
98
99impl FromStr for IdentMode {
100    type Err = Error;
101
102    fn from_str(s: &str) -> Result<Self, Self::Err> {
103        match s.to_ascii_lowercase().as_str() {
104            "uniform" => Ok(IdentMode::Uniform),
105            "indexed" => Ok(IdentMode::Indexed),
106            _ => Err(Error::InvalidConfig(format!("unknown ident mode: {s}"))),
107        }
108    }
109}
110
111/// Parameters of feature extraction.
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113pub struct Config {
114    /// Functions with fewer N0 tokens get exact hashes only (no fingerprints / MinHash).
115    pub min_tokens: usize,
116    /// How identifiers are abstracted from N1.
117    pub ident_mode: IdentMode,
118    /// k of the k-grams used for fingerprints.
119    pub kgram: usize,
120    /// Window size of winnowing.
121    pub window: usize,
122    /// Length of MinHash signatures.
123    pub minhash_size: usize,
124    /// Number of LSH bands; `minhash_size` must be divisible by it.
125    pub bands: usize,
126    /// Levels for which fingerprints, MinHash and LSH buckets are computed.
127    pub fingerprint_levels: Vec<Level>,
128    /// Keep the normalized token sequences in the output (for debugging and research).
129    pub keep_tokens: bool,
130}
131
132impl Default for Config {
133    fn default() -> Self {
134        Config {
135            min_tokens: 50,
136            ident_mode: IdentMode::Uniform,
137            kgram: 10,
138            window: 4,
139            minhash_size: 128,
140            bands: 32,
141            fingerprint_levels: vec![Level::N1],
142            keep_tokens: false,
143        }
144    }
145}
146
147impl Config {
148    /// Checks that the parameters can be used: `kgram` and `window` are positive, and
149    /// `minhash_size` is a positive multiple of `bands`.
150    ///
151    /// # Errors
152    ///
153    /// [`Error::InvalidConfig`] describing the first invalid parameter.
154    ///
155    /// # Examples
156    ///
157    /// ```
158    /// use pelicode_core::Config;
159    ///
160    /// assert!(Config::default().validate().is_ok());
161    ///
162    /// let config = Config { minhash_size: 100, bands: 32, ..Config::default() };
163    /// assert!(config.validate().is_err());
164    /// ```
165    pub fn validate(&self) -> Result<(), Error> {
166        if self.kgram == 0 || self.window == 0 {
167            return Err(Error::InvalidConfig(
168                "kgram and window must be positive".into(),
169            ));
170        }
171        if self.bands == 0
172            || self.minhash_size == 0
173            || !self.minhash_size.is_multiple_of(self.bands)
174        {
175            return Err(Error::InvalidConfig(format!(
176                "minhash_size ({}) must be a positive multiple of bands ({})",
177                self.minhash_size, self.bands
178            )));
179        }
180        Ok(())
181    }
182
183    /// Rows per LSH band.
184    ///
185    /// # Examples
186    ///
187    /// ```
188    /// use pelicode_core::Config;
189    ///
190    /// let config = Config { minhash_size: 128, bands: 32, ..Config::default() };
191    /// assert_eq!(config.rows(), 4);
192    /// ```
193    pub fn rows(&self) -> usize {
194        self.minhash_size / self.bands
195    }
196}