## 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
    }