Coverage Report

Created: 2026-05-25 08:50

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}