/home/runner/work/MathCAT/MathCAT/src/definitions.rs
Line | Count | Source |
1 | | //! # Definitions module |
2 | | //! This module is responsible for reading in the definitions files and converting them to either vectors or hashmaps so that |
3 | | //! the definitions can be used by the program. |
4 | | //! |
5 | | //! ## Leaked Implementation Details |
6 | | //! There is no escaping some implementation details. |
7 | | //! Because these definitions are stored in global variables, the variables need to be protected |
8 | | //! in some way so they can be written at runtime when the files are read. |
9 | | //! This is done by putting them inside of a lock (`thread_local`). |
10 | | //! |
11 | | //! Furthermore, it was necessary to use `RefCell` and `Rc` to deal with interior mutability. |
12 | | //! All of this means that a lock needs to be obtained _and_ the contents borrowed to access a definition. |
13 | | //! |
14 | | //! To minimize the global variable footprint, all of the definitions are put inside of a single global variable [`DEFINITIONS`]. |
15 | | //! |
16 | | //! //! Note: some of the variables are `vec`s and some are `hashset`s. |
17 | | //! Numbers are typically vectors so that indexing a digit is easy. |
18 | | //! Others such as `functions_names` are a hashset because you just want to know if an `mi` is a known name or not. |
19 | | //! The functions `as_vec` and `as_hashset` should be used on the appropriate variable. |
20 | | //! ## Names |
21 | | //! The names of "variables" in the definition files use camel case (e.g., "FunctionNames"). In the code, to fit with rust |
22 | | //! naming conventions, snake case is used (e.g, "function_names"). |
23 | | //! |
24 | | //! See the struct [`Definitions`] for the variables that are read in. |
25 | | #![allow(clippy::needless_return)] |
26 | | |
27 | | use yaml_rust::yaml::Hash; |
28 | | use yaml_rust::Yaml; |
29 | | use crate::errors::*; |
30 | | use crate::prefs::*; |
31 | | use std::{cell::RefCell, cell::Ref, cell::RefMut, rc::Rc}; |
32 | | use std::path::{Path, PathBuf}; |
33 | | use std::collections::{HashMap, HashSet}; |
34 | | use crate::shim_filesystem::read_to_string_shim; |
35 | | |
36 | | /// An enum to paper over the different types of data access needed. |
37 | | /// |
38 | | /// Having a Rc<RefCell<FromFileVariable>> seems a bit complicated in terms of types but... |
39 | | /// 1. The rust book seems to endorse the Rc<RefCell<...>>> approach when there are multiple owners of mutable date. |
40 | | /// See <https://doc.rust-lang.org/book/ch15-05-interior-mutability.html> towards the end |
41 | | /// 2. When a file is read, we need to clear and add data to the structure being read (reassigning could work for clearing). |
42 | | /// When we use the data, we either want to index into it or test if an item is there. |
43 | | /// The structures we use are either a Vec or a HashMap, so we need to abstract that away in `FromFileVariable`. |
44 | | /// Unfortunately, traits don't quite work as an option here: |
45 | | /// * Vec implements extends (`add`), but there is no test/contains |
46 | | /// * Hashmap implements `index`, but panics if the item isn't there |
47 | | /// |
48 | | /// Because of the above limitations, we introduce the enum [`Contains`] which dispatches appropriately to Vec/Hashmap |
49 | | #[derive(Debug, Clone)] |
50 | | pub enum Contains { |
51 | | Vec(Rc<RefCell<Vec<String>>>), |
52 | | Set(Rc<RefCell<HashSet<String>>>), |
53 | | Map(Rc<RefCell<HashMap<String, String>>>), |
54 | | } |
55 | | |
56 | | impl Contains { |
57 | | // fn add(&mut self, item: String) { |
58 | | // match self { |
59 | | // Contains::Vec(v) => { v.borrow_mut().push(item); }, |
60 | | // Contains::Set(s) => { s.borrow_mut().insert(item); } |
61 | | // } |
62 | | // } |
63 | | |
64 | | // fn clear(&mut self) { |
65 | | // match self { |
66 | | // Contains::Vec(v) => { v.borrow_mut().clear(); }, |
67 | | // Contains::Set(s) => { s.borrow_mut().clear(); } |
68 | | // } |
69 | | // } |
70 | | } |
71 | | pub type CollectionFromFile = Contains; |
72 | | type VariableDefHashMap = HashMap<String, CollectionFromFile>; |
73 | | |
74 | | /// Global structure containing all of the definitions. |
75 | | /// Each field in the structure corresponds to a named value read in from the `definitions.yaml` files. |
76 | | /// |
77 | | /// The names of "variables" in the definition files use camel case (e.g., "FunctionNames"). In the code, to fit with rust |
78 | | /// naming conventions, snake case is used (e.g, "function_names"). |
79 | | /// |
80 | | /// There should only be one instance of this structure ([`DEFINITIONS`]) |
81 | | // FIX: this probably can done with a macro to remove all the repetition |
82 | | pub struct Definitions { |
83 | | pub name_to_var_mapping: VariableDefHashMap, |
84 | | } |
85 | | |
86 | | impl Default for Definitions { |
87 | 0 | fn default() -> Self { |
88 | 0 | Definitions { |
89 | 0 | name_to_var_mapping: HashMap::with_capacity(30), |
90 | 0 | } |
91 | 0 | } |
92 | | } |
93 | | |
94 | | impl Definitions { |
95 | 5.49k | fn new() -> Self { |
96 | 5.49k | Definitions { |
97 | 5.49k | name_to_var_mapping: HashMap::with_capacity(30), |
98 | 5.49k | } |
99 | 5.49k | } |
100 | | |
101 | 158k | pub fn get_hashset(&self, name: &str) -> Option<Ref<'_, HashSet<String>>> { |
102 | 158k | let names = self.name_to_var_mapping.get(name); |
103 | 151k | if let Some(Contains::Set(set151k )) = names { |
104 | 151k | return Some(set.borrow()); |
105 | 7.30k | } |
106 | 7.30k | return None; |
107 | 158k | } |
108 | | |
109 | 15.9k | pub fn get_hashmap(&self, name: &str) -> Option<Ref<'_, HashMap<String, String>>> { |
110 | 15.9k | let names = self.name_to_var_mapping.get(name); |
111 | 15.9k | if let Some(Contains::Map(map)) = names { |
112 | 15.9k | return Some(map.borrow()); |
113 | 0 | } |
114 | 0 | return None; |
115 | 15.9k | } |
116 | | |
117 | 1.31k | pub fn get_vec(&self, name: &str) -> Option<Ref<'_, Vec<String>>> { |
118 | 1.31k | let names = self.name_to_var_mapping.get(name); |
119 | 1.31k | if let Some(Contains::Vec(vec)) = names { |
120 | 1.31k | return Some(vec.borrow()); |
121 | 0 | } |
122 | 0 | return None; |
123 | 1.31k | } |
124 | | } |
125 | | |
126 | | thread_local!{ |
127 | | /// Global variable containing all of the definitions. |
128 | | /// See [`Definitions`] for more details. |
129 | | pub static SPEECH_DEFINITIONS: RefCell<Definitions> = RefCell::new( Definitions::new() ); |
130 | | pub static BRAILLE_DEFINITIONS: RefCell<Definitions> = RefCell::new( Definitions::new() ); |
131 | | pub static DEFINITIONS: &'static std::thread::LocalKey<RefCell<Definitions>> = const { &SPEECH_DEFINITIONS }; |
132 | | } |
133 | | |
134 | | /// Reads the `definitions.yaml` files specified by current_files -- these are presumed to need updating. |
135 | | /// |
136 | | /// If there is a failure during read, the error is propagated to the caller |
137 | 5.50k | pub fn read_definitions_file(use_speech_defs: bool) -> Result<Vec<PathBuf>> { |
138 | | // for each file in `locations`, read the contents and process them |
139 | 5.50k | let pref_manager = PreferenceManager::get(); |
140 | 5.50k | let pref_manager = pref_manager.borrow(); |
141 | 5.50k | let file_path = pref_manager.get_definitions_file(use_speech_defs); |
142 | 5.50k | let definitions = if use_speech_defs {&SPEECH_DEFINITIONS4.15k } else {&BRAILLE_DEFINITIONS1.35k }; |
143 | 5.50k | definitions.with( |defs| defs.borrow_mut().name_to_var_mapping.clear() ); |
144 | 5.50k | let mut new_files = vec![file_path.to_path_buf()]; |
145 | 5.50k | let mut files_read = read_one_definitions_file(use_speech_defs, file_path).with_context(|| format!0 ("in file '{}", file_path0 .to_string_lossy0 ()))?0 ; |
146 | 5.50k | new_files.append(&mut files_read); |
147 | | |
148 | | // merge the contents of `TrigFunctions` into a set that contains all the function names (from `AdditionalFunctionNames`). |
149 | 5.50k | return definitions.with(|defs| { |
150 | 5.50k | let mut defs = defs.borrow_mut(); |
151 | 5.50k | make_all_set_references_valid(&mut defs); |
152 | 5.50k | return Ok(new_files); |
153 | 5.50k | }); |
154 | | |
155 | | |
156 | | /// Make references to all used set be valid by creating empty sets if they weren't defined |
157 | 5.50k | fn make_all_set_references_valid(defs: &mut RefMut<Definitions>) { |
158 | | // FIX: this list is created by hand -- it would be better if there was a way to create the list Automatically |
159 | | // Note: "FunctionNames" is created in build_all_functions_set() if not already set |
160 | 5.50k | let used_set_names = ["GeometryPrefixOperators", "LikelyFunctionNames", "TrigFunctionNames", "AdditionalFunctionNames", "Arrows", "GeometryShapes"]; |
161 | | // let name_to_mapping = defs.name_to_var_mapping.borrow_mut(); |
162 | 33.0k | for set_name in used_set_names5.50k { |
163 | 33.0k | if defs.get_hashset(set_name).is_none() { |
164 | 1.74k | defs.name_to_var_mapping.insert(set_name.to_string(), Contains::Set( Rc::new( RefCell::new( HashSet::with_capacity(0) ) ) )); |
165 | 31.3k | } |
166 | | } |
167 | 5.50k | if defs.get_hashset("FunctionNames").is_none() { |
168 | 5.46k | let all_functions = build_all_functions_set(defs); |
169 | 5.46k | defs.name_to_var_mapping.insert("FunctionNames".to_string(), Contains::Set( Rc::new( RefCell::new( all_functions ) ) )); |
170 | 5.46k | }41 |
171 | 5.50k | } |
172 | | |
173 | | /// merge "TrigFunctions" and "AdditionalFunctionNames" into a new set named "FunctionNames" |
174 | 5.46k | fn build_all_functions_set(defs: &mut RefMut<Definitions>) -> HashSet<String> { |
175 | 5.46k | let trig_functions = defs.get_hashset("TrigFunctionNames").unwrap(); |
176 | 5.46k | let mut all_functions = defs.get_hashset("AdditionalFunctionNames").unwrap().clone(); |
177 | 109k | for trig_name in trig_functions.iter()5.46k { |
178 | 109k | all_functions.insert(trig_name.clone()); |
179 | 109k | } |
180 | 5.46k | return all_functions; |
181 | 5.46k | } |
182 | 5.50k | } |
183 | | |
184 | | use crate::speech::*; |
185 | 11.7k | fn read_one_definitions_file(use_speech_defs: bool, path: &Path) -> Result<Vec<PathBuf>> { |
186 | | // read in the file contents |
187 | 11.7k | let definition_file_contents = read_to_string_shim(path) |
188 | 11.7k | .with_context(|| format!0 ("trying to read {}", path0 .to_str0 ().unwrap0 ()))?0 ; |
189 | | |
190 | | // callback to do the work of building up the defined vectors/hashmaps (in 'build_values') from YAML |
191 | 11.7k | let defs_build_fn = |variable_def_list: &Yaml| { |
192 | | // Rule::DefinitionList |
193 | | // debug!("variable_def_list {} is\n{}", yaml_to_type(variable_def_list), yaml_to_string(variable_def_list)); |
194 | 11.7k | let mut files_read = vec![path.to_path_buf()]; |
195 | 11.7k | let vec = crate::speech::as_vec_checked(variable_def_list) |
196 | 11.7k | .with_context(||format!0 ("in file {:?}", path0 .to_str0 ()))?0 ; |
197 | 175k | for variable_def in vec11.7k { |
198 | 175k | if let Some(mut added_files6.28k ) = build_values(variable_def, use_speech_defs, path).with_context(||format!0 ("in file {:?}", path0 .to_str0 ()))?0 { |
199 | 6.28k | files_read.append(&mut added_files); |
200 | 169k | } |
201 | | } |
202 | 11.7k | return Ok(files_read); |
203 | 11.7k | }; |
204 | | |
205 | | // Convert the file contents to YAML and call the callback |
206 | 11.7k | return crate::speech::compile_rule(&definition_file_contents, defs_build_fn) |
207 | 11.7k | .with_context(|| format!0 ("In file '{}'", path0 .to_str0 ().unwrap0 ())); |
208 | 11.7k | } |
209 | | |
210 | | /// Do the work of converting a single YAML def into the vec/hashset/hashmap |
211 | | /// name: [a, b, c] -- assume an indexed vector |
212 | | /// name: {a, b, c} -- assume a hash set |
213 | | /// name: {a: A, b: B, c: C} -- assume a hashmap |
214 | | /// Returns all the files that were read |
215 | 175k | fn build_values(definition: &Yaml, use_speech_defs: bool, path: &Path) -> Result<Option<Vec<PathBuf>>> { |
216 | | // Rule::Definition |
217 | 175k | let dictionary = crate::speech::as_hash_checked(definition)?0 ; |
218 | 175k | if dictionary.len()!=1 { |
219 | 0 | bail!("Should only be one definition rule: {}", yaml_to_type(definition)); |
220 | 175k | } |
221 | 175k | let (key, value) = dictionary.iter().next().unwrap(); |
222 | 175k | let def_name = key.as_str().ok_or_else(|| anyhow!0 ("definition list name '{}' is not a string", yaml_to_type0 (key0 )))?0 ; |
223 | 175k | if def_name == "include" { |
224 | 6.28k | let do_include_fn = |new_file: &Path| { |
225 | 6.28k | read_one_definitions_file(use_speech_defs, new_file) |
226 | 6.28k | }; |
227 | 6.28k | let include_file_name = value.as_str().ok_or_else(|| anyhow!0 ("definition list include name '{}' is not a string", yaml_to_type0 (value0 )))?0 ; |
228 | 6.28k | return Ok( Some(crate::speech::process_include(path, include_file_name, do_include_fn)?0 ) ); |
229 | 169k | } |
230 | | |
231 | | let result; |
232 | 169k | if def_name.starts_with("Numbers") || def_name111k .ends_with111k ("_vec") { |
233 | 58.1k | result = Contains::Vec( Rc::new( RefCell::new( get_vec_values(value.as_vec().unwrap())?0 ) ) ); |
234 | | } else { |
235 | | // match value.as_vec() { |
236 | | // Some(vec) => { |
237 | | // result = Contains::Set( Rc::new( RefCell::new( get_set_values(vec)? ) ) ); }, |
238 | | // None => { |
239 | | // let dict = value.as_hash().ok_or_else(|| anyhow!("definition list value '{}' is not an array or dictionary", yaml_to_type(value)))?; |
240 | | // result = Contains::Map( Rc::new( RefCell::new( get_map_values(dict) |
241 | | // .chain_err(||format!("while reading value '{}'", def_name))? ) ) ); |
242 | | |
243 | | // }, |
244 | | // } |
245 | 111k | let dict = value.as_hash().ok_or_else(|| anyhow!0 ("definition list value '{}' is not an array or dictionary", yaml_to_type0 (value0 )))?0 ; |
246 | 111k | if dict.is_empty() { |
247 | 15.6k | result = Contains::Set( Rc::new( RefCell::new( HashSet::with_capacity(0) ) ) ); |
248 | 15.6k | } else { |
249 | | // peak and see if this is a set or a map |
250 | 95.4k | let (_, entry_value) = dict.iter().next().unwrap(); |
251 | 95.4k | if entry_value.is_null() { |
252 | 63.5k | result = Contains::Set( Rc::new( RefCell::new( get_set_values(dict) |
253 | 63.5k | .with_context(||format!0 ("while reading value '{def_name}'"))?0 ) ) ); |
254 | | } else { |
255 | | // peak and see if this is a set or a map |
256 | 31.8k | let (_, entry_value) = dict.iter().next().unwrap(); |
257 | 31.8k | if entry_value.is_null() { |
258 | 0 | result = Contains::Set( Rc::new( RefCell::new( get_set_values(dict) |
259 | 0 | .with_context(||format!("while reading value '{def_name}'"))? ) ) ); |
260 | | } else { |
261 | 31.8k | result = Contains::Map( Rc::new( RefCell::new( get_map_values(dict) |
262 | 31.8k | .with_context(||format!0 ("while reading value '{def_name}'"))?0 ) ) ); |
263 | | } |
264 | | } |
265 | | } |
266 | | }; |
267 | | |
268 | 169k | let definitions = if use_speech_defs {&SPEECH_DEFINITIONS149k } else {&BRAILLE_DEFINITIONS19.3k }; |
269 | 169k | return definitions.with(|definitions| { |
270 | 169k | let name_definition_map = &mut definitions.borrow_mut().name_to_var_mapping; |
271 | 169k | name_definition_map.insert(def_name.to_string(), result); |
272 | 169k | return Ok(None); |
273 | 169k | }); |
274 | | |
275 | 58.1k | fn get_vec_values(values: &Vec<Yaml>) -> Result<Vec<String>> { |
276 | 58.1k | let mut result = Vec::with_capacity(values.len()); |
277 | 788k | for yaml_value in values58.1k { |
278 | 788k | let value = yaml_value.as_str() |
279 | 788k | .ok_or_else(|| anyhow!0 ("list entry '{}' is not a string", yaml_to_type0 (yaml_value0 )))?0 |
280 | 788k | .to_string(); |
281 | 788k | result.push(value); |
282 | | } |
283 | 58.1k | return Ok(result); |
284 | 58.1k | } |
285 | | |
286 | 63.5k | fn get_set_values(values: &Hash) -> Result<HashSet<String>> { |
287 | 63.5k | let mut result = HashSet::with_capacity(2*values.len()); |
288 | 5.82M | for (key, value) in values63.5k { |
289 | 5.82M | let key = key.as_str() |
290 | 5.82M | .ok_or_else(|| anyhow!0 ("list entry '{}' is not a string", yaml_to_type0 (key0 )))?0 |
291 | 5.82M | .to_string(); |
292 | 5.82M | if let Yaml::Null = value { |
293 | 5.82M | } else { |
294 | 0 | bail!("list entry '{}' is not a string", yaml_to_type(value)); |
295 | | } |
296 | 5.82M | result.insert(key); |
297 | | } |
298 | 63.5k | return Ok(result); |
299 | 63.5k | } |
300 | | |
301 | 31.8k | fn get_map_values(values: &Hash) -> Result<HashMap<String, String>> { |
302 | 31.8k | let mut result = HashMap::with_capacity(2*values.len()); |
303 | 1.13M | for (key, value) in values31.8k { |
304 | 1.13M | let key = key.as_str() |
305 | 1.13M | .ok_or_else(|| anyhow!0 ("list entry '{}' is not a string", yaml_to_type0 (key0 )))?0 |
306 | 1.13M | .to_string(); |
307 | 1.13M | let value = value.as_str() |
308 | 1.13M | .ok_or_else(|| anyhow!0 ("list entry '{}' is not a string", yaml_to_type0 (value0 )))?0 |
309 | 1.13M | .to_string(); |
310 | 1.13M | result.insert(key, value); |
311 | | } |
312 | 31.8k | return Ok(result); |
313 | 31.8k | } |
314 | 175k | } |
315 | | |
316 | | |
317 | | #[cfg(test)] |
318 | | mod tests { |
319 | | use super::*; |
320 | | |
321 | | #[test] |
322 | 1 | fn test_vec() { |
323 | 1 | let numbers = r#"[NumbersTens: ["", "ten", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"]]"#; |
324 | 1 | let defs_build_fn = |variable_def_list: &Yaml| { |
325 | | // Rule::DefinitionList |
326 | | //debug!("variable_def_list {} is\n{}", yaml_to_type(variable_def_list), yaml_to_string(variable_def_list, 0)); |
327 | 1 | for variable_def in variable_def_list.as_vec().unwrap() { |
328 | 1 | if let Err(e0 ) = build_values(variable_def, true, Path::new("")) { |
329 | 0 | bail!("{}", crate::interface::errors_to_string(&e.context(format!("in file {:?}", numbers)))); |
330 | 1 | } |
331 | | } |
332 | 1 | return Ok(vec![]); |
333 | 1 | }; |
334 | 1 | compile_rule(numbers, defs_build_fn).unwrap(); |
335 | 1 | SPEECH_DEFINITIONS.with(|defs| { |
336 | 1 | let defs = defs.borrow(); |
337 | 1 | let names = defs.get_vec("NumbersTens"); |
338 | 1 | assert!(names.is_some()); |
339 | 1 | let names = names.unwrap(); |
340 | 1 | assert_eq!(names.len(), 10); |
341 | 1 | assert_eq!(names[0], ""); |
342 | 1 | assert_eq!(names[9], "ninety"); |
343 | 1 | }); |
344 | 1 | } |
345 | | |
346 | | |
347 | | #[test] |
348 | 1 | fn test_set() { |
349 | 1 | let likely_function_names = r#"[LikelyFunctionNames: {"f", "g", "h", "F", "G", "H", "[A-Za-z]+"}]"#; |
350 | 1 | let defs_build_fn = |variable_def_list: &Yaml| { |
351 | | // Rule::DefinitionList |
352 | | //debug!("variable_def_list {} is\n{}", yaml_to_type(variable_def_list), yaml_to_string(variable_def_list, 0)); |
353 | 1 | for variable_def in variable_def_list.as_vec().unwrap() { |
354 | 1 | if let Err(e0 ) = build_values(variable_def, true, Path::new("")) { |
355 | 0 | bail!("{}", crate::interface::errors_to_string(&e.context(format!("in file {:?}", likely_function_names)))); |
356 | 1 | } |
357 | | } |
358 | 1 | return Ok(vec![]); |
359 | 1 | }; |
360 | 1 | compile_rule(likely_function_names, defs_build_fn).unwrap(); |
361 | 1 | SPEECH_DEFINITIONS.with(|defs| { |
362 | 1 | let defs = defs.borrow(); |
363 | 1 | let names = defs.get_hashset("LikelyFunctionNames"); |
364 | 1 | assert!(names.is_some()); |
365 | 1 | let names = names.unwrap(); |
366 | 1 | assert_eq!(names.len(), 7); |
367 | 1 | assert!(names.contains("f")); |
368 | 1 | assert!(!names.contains("a")); |
369 | 1 | }); |
370 | 1 | } |
371 | | |
372 | | #[test] |
373 | 1 | fn test_hashmap() { |
374 | 1 | let units = r#"[Units: {"A": "amp", "g": "gram", "m": "meter", "sec": "second"}]"#; |
375 | 1 | let defs_build_fn = |variable_def_list: &Yaml| { |
376 | | // Rule::DefinitionList |
377 | | //debug!("variable_def_list {} is\n{}", yaml_to_type(variable_def_list), yaml_to_string(variable_def_list, 0)); |
378 | 1 | for variable_def in variable_def_list.as_vec().unwrap() { |
379 | 1 | if let Err(e0 ) = build_values(variable_def, true, Path::new("")) { |
380 | 0 | bail!("{}", crate::interface::errors_to_string(&e.context(format!("in file {:?}", units)))); |
381 | 1 | } |
382 | | } |
383 | 1 | return Ok(vec![]); |
384 | 1 | }; |
385 | 1 | compile_rule(units, defs_build_fn).unwrap(); |
386 | 1 | SPEECH_DEFINITIONS.with(|defs| { |
387 | 1 | let defs = defs.borrow(); |
388 | 1 | let names = defs.get_hashmap("Units"); |
389 | 1 | assert!(names.is_some()); |
390 | 1 | let names = names.unwrap(); |
391 | 1 | assert_eq!(names.len(), 4); |
392 | 1 | assert_eq!(names.get("A").unwrap(), "amp"); |
393 | 1 | assert_eq!(names.get("sec").unwrap(), "second"); |
394 | 1 | assert_eq!(names.get("xxx"), None); |
395 | 1 | }); |
396 | 1 | } |
397 | | } |