donet_core/
dcfile.rs

1/*
2    This file is part of Donet.
3
4    Copyright © 2024 Max Rodriguez <[email protected]>
5
6    Donet is free software; you can redistribute it and/or modify
7    it under the terms of the GNU Affero General Public License,
8    as published by the Free Software Foundation, either version 3
9    of the License, or (at your option) any later version.
10
11    Donet is distributed in the hope that it will be useful,
12    but WITHOUT ANY WARRANTY; without even the implied warranty of
13    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14    GNU Affero General Public License for more details.
15
16    You should have received a copy of the GNU Affero General Public
17    License along with Donet. If not, see <https://www.gnu.org/licenses/>.
18*/
19
20//! Root structure that stores the collection of DC elements
21//! in memory. Provides functions for manipulating the tree.
22
23use crate::dcfield::DCField;
24use crate::dckeyword::DCKeyword;
25use crate::dclass::DClass;
26use crate::dconfig::*;
27use crate::dcstruct::DCStruct;
28use crate::dctype::DCTypeDefinition;
29use crate::globals;
30use crate::hashgen::*;
31use crate::parser::ast;
32
33/// Represents a Python-style import statement in the DC file.
34#[derive(Debug, Clone)]
35pub struct DCPythonImport {
36    pub module: String,
37    pub symbols: Vec<String>,
38}
39
40impl From<interim::PythonImport> for DCPythonImport {
41    fn from(value: interim::PythonImport) -> Self {
42        Self {
43            module: value.module,
44            symbols: value.symbols,
45        }
46    }
47}
48
49impl std::fmt::Display for DCPythonImport {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        if self.symbols.is_empty() {
52            write!(f, "import ")?;
53            f.write_str(&self.module)?;
54        } else {
55            write!(f, "from ")?;
56            f.write_str(&self.module)?;
57
58            write!(f, " import ")?;
59            for (i, symbol) in self.symbols.iter().enumerate() {
60                f.write_str(symbol)?;
61
62                if i != self.symbols.len() - 1 {
63                    write!(f, ", ")?;
64                }
65            }
66        }
67        Ok(())
68    }
69}
70
71/// Data model that provides a high level representation of a single,
72/// or collection, of DC files and their elements such as class imports,
73/// type definitions, structures, and Distributed Classes.
74#[derive(Debug, Clone)]
75pub struct DCFile<'dc> {
76    config: DCFileConfig,
77    baked_legacy_hash: globals::DCFileHash,
78    structs: Vec<DCStruct<'dc>>,
79    dclasses: Vec<DClass<'dc>>,
80    imports: Vec<DCPythonImport>,
81    keywords: Vec<DCKeyword>,
82    type_defs: Vec<DCTypeDefinition>,
83    field_id_2_field: Vec<&'dc DCField<'dc>>,
84    // TODO: type_id_2_type, type_name_2_type
85    all_object_valid: bool,
86    inherited_fields_stale: bool,
87}
88
89impl From<interim::DCFile> for DCFile<'_> {
90    fn from(value: interim::DCFile) -> Self {
91        let mut imports: Vec<DCPythonImport> = vec![];
92        let mut keywords: Vec<DCKeyword> = vec![];
93
94        for imp in value.imports {
95            imports.push(imp.into());
96        }
97
98        for kw in value.keywords {
99            keywords.push(kw.into());
100        }
101
102        Self {
103            config: value.config,
104            baked_legacy_hash: 0_u32,
105            structs: vec![],
106            dclasses: vec![],
107            imports,
108            keywords,
109            type_defs: vec![],
110            field_id_2_field: vec![],
111            all_object_valid: true,
112            inherited_fields_stale: false,
113        }
114    }
115}
116
117impl std::fmt::Display for DCFile<'_> {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        // output dc parser configuration variables used
120        f.write_str(&self.config.to_string())?;
121
122        // Print Python-style imports
123        if !self.imports.is_empty() {
124            for import in &self.imports {
125                import.fmt(f)?;
126                writeln!(f)?;
127            }
128            writeln!(f)?;
129        }
130        // Print type definitions
131        for type_def in &self.type_defs {
132            type_def.fmt(f)?;
133            writeln!(f)?;
134        }
135        // Print Keyword definitions
136        for kw in &self.keywords {
137            kw.fmt(f)?;
138            writeln!(f)?;
139        }
140        // Print Structs
141        for strukt in &self.structs {
142            strukt.fmt(f)?;
143            writeln!(f)?;
144        }
145        // Print DClasses
146        for dclass in &self.dclasses {
147            dclass.fmt(f)?;
148            writeln!(f)?;
149        }
150        Ok(())
151    }
152}
153
154impl DCFileConfigAccessor for DCFile<'_> {
155    fn get_dc_config(&self) -> &DCFileConfig {
156        &self.config
157    }
158}
159
160impl LegacyDCHash for DCFile<'_> {
161    fn generate_hash(&self, hashgen: &mut DCHashGenerator) {
162        if self.config.dc_virtual_inheritance {
163            // Just to change the hash output in this case.
164            if self.config.dc_sort_inheritance_by_file {
165                hashgen.add_int(1);
166            } else {
167                hashgen.add_int(2);
168            }
169        }
170        hashgen.add_int(self.get_num_dclasses().try_into().unwrap());
171
172        for strukt in &self.structs {
173            strukt.generate_hash(hashgen);
174        }
175
176        for dclass in &self.dclasses {
177            dclass.generate_hash(hashgen);
178        }
179    }
180}
181
182impl<'dc> DCFile<'dc> {
183    /// Returns a 32-bit hash index associated with this file.  This number is
184    /// guaranteed to be consistent if the contents of the file have not changed,
185    /// and it is very likely to be different if the contents of the file do change.
186    ///
187    /// If called more than once, it will reuse the already calculated hash,
188    /// as this structure is guaranteed to be immutable after initialization.
189    pub fn get_legacy_hash(&self) -> globals::DCFileHash {
190        if self.baked_legacy_hash != 0 {
191            self.baked_legacy_hash
192        } else {
193            let mut hashgen: DCHashGenerator = DCHashGenerator::default();
194
195            self.generate_hash(&mut hashgen);
196            hashgen.get_hash()
197        }
198    }
199
200    /// Returns a string with the hash as a pretty format hexadecimal.
201    pub fn get_pretty_hash(&self) -> String {
202        format!("0x{:0width$x}", self.get_legacy_hash(), width = 8) // 2 hex / byte = 8 hex
203    }
204
205    // ---------- Python Imports ---------- //
206
207    pub fn get_num_imports(&self) -> usize {
208        self.imports.len()
209    }
210
211    pub fn get_python_import(&self, index: usize) -> &DCPythonImport {
212        self.imports.get(index).expect("Index out of bounds.")
213    }
214
215    // ---------- DC Keyword ---------- //
216
217    pub fn get_num_keywords(&self) -> usize {
218        todo!();
219    }
220
221    pub fn get_keyword(&self, _index: usize) -> &'dc DCKeyword {
222        todo!();
223    }
224
225    pub fn has_keyword(&self, _keyword: String) -> bool {
226        todo!();
227    }
228
229    // ---------- Distributed Class ---------- //
230
231    pub fn get_num_dclasses(&self) -> usize {
232        self.dclasses.len()
233    }
234
235    pub fn get_dclass(&self, _index: usize) -> &'dc DClass {
236        todo!();
237    }
238
239    pub fn get_dclass_by_id(&self, id: globals::DClassId) -> &'dc DClass {
240        self.dclasses.get(usize::from(id)).unwrap()
241    }
242
243    pub fn get_dclass_by_name(&self, _name: &str) -> &'dc DClass {
244        todo!();
245    }
246
247    // ---------- DC Struct ---------- //
248
249    pub fn get_num_structs(&self) -> usize {
250        todo!();
251    }
252
253    pub fn get_struct(&self, _index: usize) -> &'dc DCStruct {
254        todo!();
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn write_dc_python_import() {
264        let import: DCPythonImport = DCPythonImport {
265            module: "views".to_string(),
266            symbols: vec![],
267        };
268
269        assert_eq!(import.to_string(), "import views");
270    }
271
272    #[test]
273    fn write_dcfile_py_imports() {
274        let imports: Vec<DCPythonImport> = vec![
275            DCPythonImport {
276                module: "views".to_string(),
277                symbols: vec![],
278            },
279            DCPythonImport {
280                module: "views".to_string(),
281                symbols: vec!["DistributedDonut".to_string()],
282            },
283            DCPythonImport {
284                module: "views".to_string(),
285                symbols: vec!["Class".to_string(), "ClassAI".to_string(), "ClassOV".to_string()],
286            },
287        ];
288
289        let dcf: DCFile<'_> = DCFile {
290            config: DCFileConfig::default(),
291            baked_legacy_hash: 0_u32,
292            structs: vec![],
293            dclasses: vec![],
294            imports,
295            keywords: vec![],
296            type_defs: vec![],
297            field_id_2_field: vec![],
298            all_object_valid: false,
299            inherited_fields_stale: false,
300        };
301
302        assert_eq!(
303            dcf.to_string(),
304            "\
305            /*\n\
306            DC_MULTIPLE_INHERITANCE = true\n\
307            DC_SORT_INHERITANCE_BY_FILE = true\n\
308            DC_VIRTUAL_INHERITANCE = true\n\
309            */\n\n\
310            import views\n\
311            from views import DistributedDonut\n\
312            from views import Class, ClassAI, ClassOV\n\
313            \n\
314            ",
315        );
316    }
317}
318
319/// Contains intermediate DC file structure and logic
320/// for semantic analysis as the DC file is being built.
321pub(crate) mod interim {
322    use super::{ast, globals, DCField, DCFileConfig};
323    use crate::dckeyword::interim::DCKeyword;
324    use crate::dclass::interim::DClass;
325    use crate::dcstruct::interim::DCStruct;
326    use crate::parser::error::{Diagnostic, SemanticError};
327    use crate::parser::pipeline::PipelineData;
328    use anyhow::{anyhow, Result};
329    use std::collections::HashSet;
330
331    #[derive(Debug)]
332    pub struct PythonImport {
333        pub module: String,
334        pub symbols: Vec<String>,
335    }
336
337    /// DC file structure for internal use by the DC parser.
338    #[derive(Debug)]
339    pub(crate) struct DCFile {
340        pub config: DCFileConfig,
341        pub structs: Vec<DCStruct>,
342        pub dclasses: Vec<DClass>,
343        pub imports: Vec<PythonImport>,
344        pub keywords: Vec<DCKeyword>,
345        //pub field_id_2_field: Vec<Rc<DCField>>,
346        // TODO: type_id_2_type, type_name_2_type
347        pub all_object_valid: bool,
348        pub inherited_fields_stale: bool,
349    }
350
351    impl From<DCFileConfig> for DCFile {
352        fn from(value: DCFileConfig) -> Self {
353            Self {
354                config: value,
355                structs: vec![],
356                dclasses: vec![],
357                imports: vec![],
358                keywords: vec![],
359                //field_id_2_field: vec![],
360                all_object_valid: true,
361                inherited_fields_stale: false,
362            }
363        }
364    }
365
366    impl DCFile {
367        /// Assigns unique ID to the field for the scope of the entire DC file.
368        pub fn add_field(&mut self, _field: DCField) {
369            todo!();
370        }
371
372        /// Redundancy check for an array of strings that represent view suffixes.
373        /// The lexer already generates a specific token type for view suffixes,
374        /// and the parser grammar expects this token type, so we already are
375        /// guaranteed that the view suffixes are valid.
376        fn check_view_suffixes(pipeline: &mut PipelineData, view_suffixes: &ast::ViewSuffixes) {
377            let mut recorded_suffixes: HashSet<String> = HashSet::default();
378
379            for view_suffix in view_suffixes {
380                if !recorded_suffixes.insert(view_suffix.view.clone()) {
381                    let diag: Diagnostic = Diagnostic::error(
382                        view_suffix.span,
383                        pipeline,
384                        SemanticError::RedundantViewSuffix(view_suffix.view.clone()),
385                    );
386
387                    pipeline
388                        .emit_diagnostic(diag.into())
389                        .expect("Failed to emit diagnostic.");
390                }
391            }
392        }
393
394        /// 'Untangles' a [`ast::PythonImport`], which represents a python import line,
395        /// into one or more [`PythonImport`] structures, which represent symbol imports
396        /// from a python module (with view suffixes applied) and adds them to the DC file.
397        pub fn add_python_import(&mut self, pipeline: &mut PipelineData, import: ast::PythonImport) {
398            let mut imports: Vec<PythonImport> = vec![];
399            let mut class_symbols: Vec<String> = vec![import.class.symbol.clone()];
400
401            // check view suffixes
402            Self::check_view_suffixes(pipeline, &import.module.symbol_views);
403            Self::check_view_suffixes(pipeline, &import.class.symbol_views);
404
405            // Separates "Class/AI/OV" to ["Class", "ClassAI", "ClassOV"]
406            if !import.class.symbol_views.is_empty() {
407                for class_suffix in &import.class.symbol_views {
408                    class_symbols.push(import.class.symbol.clone() + &class_suffix.view);
409                }
410            }
411
412            // Handles e.g. "from module/AI/OV/UD import DistributedThing/AI/OV/UD"
413            if !import.module.symbol_views.is_empty() {
414                let mut c_symbol: String = class_symbols.first().unwrap().clone();
415
416                imports.push(PythonImport {
417                    module: import.module.symbol.clone(),
418                    symbols: vec![c_symbol],
419                });
420
421                for (i, module_suffix) in import.module.symbol_views.into_iter().enumerate() {
422                    let full_import: String = import.module.symbol.clone() + &module_suffix.view;
423
424                    if (class_symbols.len() - 1) <= i {
425                        c_symbol = class_symbols.last().unwrap().clone();
426                    } else {
427                        c_symbol = class_symbols.get(i + 1).unwrap().clone();
428                    }
429
430                    imports.push(PythonImport {
431                        module: full_import,
432                        symbols: vec![c_symbol],
433                    });
434                }
435            } else {
436                // No view suffixes for the module symbol, so just push the symbol.
437                imports.push(PythonImport {
438                    module: import.module.symbol,
439                    symbols: class_symbols,
440                });
441            }
442
443            for imp in imports {
444                self.imports.push(imp);
445            }
446        }
447
448        pub fn add_keyword(&mut self, pipeline: &mut PipelineData, keyword: ast::KeywordDefinition) {
449            // convert from AST node to its interim struct
450            let new_kw: DCKeyword = keyword.into();
451
452            for kw in &self.keywords {
453                if kw.name == new_kw.name {
454                    let diag: Diagnostic = Diagnostic::error(
455                        new_kw.span,
456                        pipeline,
457                        SemanticError::AlreadyDefined(new_kw.name.clone()),
458                    );
459
460                    pipeline
461                        .emit_diagnostic(diag.into())
462                        .expect("Failed to emit diagnostic.");
463                    return;
464                }
465            }
466            self.keywords.push(new_kw);
467        }
468
469        pub fn add_typedef(&mut self, _name: String) -> Result<(), ()> {
470            todo!();
471        }
472
473        pub fn add_dclass(&mut self, dclass: DClass) {
474            self.dclasses.push(dclass);
475        }
476
477        pub fn add_struct(&mut self, _strct: DCStruct) {
478            todo!();
479        }
480
481        /// Gets the next dclass ID based on the current allocated IDs.
482        ///
483        /// If an error is returned, this DC file has run out of dclass
484        /// IDs to assign. This function will emit the error diagnostic.
485        ///
486        pub fn get_next_dclass_id(
487            &mut self,
488            pipeline: &mut PipelineData,
489            dclass: &DClass, // current dclass ref for diagnostic span
490        ) -> Result<globals::DClassId> {
491            let dc_num: u16 = self.dclasses.len().try_into().unwrap();
492
493            if dc_num == globals::DClassId::MAX {
494                // We have reached the maximum number of dclass declarations.
495                let diag: Diagnostic =
496                    Diagnostic::error(dclass.span, pipeline, SemanticError::DClassOverflow);
497
498                pipeline
499                    .emit_diagnostic(diag.into())
500                    .expect("Failed to emit diagnostic.");
501
502                return Err(anyhow!("Ran out of 16-bit DClass IDs!"));
503            }
504            Ok(dc_num - 1_u16)
505        }
506    }
507}