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}