donet_core/
dcfile.rs

1/*
2    This file is part of Donet.
3
4    Copyright © 2024-2025 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::dckeyword::DCKeyword;
24use crate::dclass::DClass;
25use crate::dcstruct::DCStruct;
26use crate::dctype::DCTypeDefinition;
27use crate::globals;
28use crate::hashgen::*;
29use crate::parser::ast;
30
31/// Represents a Python-style import statement in the DC file.
32#[derive(Debug, Clone)]
33pub struct DCPythonImport {
34    pub module: String,
35    pub symbols: Vec<String>,
36}
37
38impl std::fmt::Display for DCPythonImport {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        if self.symbols.is_empty() {
41            write!(f, "import ")?;
42            f.write_str(&self.module)?;
43        } else {
44            write!(f, "from ")?;
45            f.write_str(&self.module)?;
46
47            write!(f, " import ")?;
48            for (i, symbol) in self.symbols.iter().enumerate() {
49                f.write_str(symbol)?;
50
51                if i != self.symbols.len() - 1 {
52                    write!(f, ", ")?;
53                }
54            }
55        }
56        Ok(())
57    }
58}
59
60/// Data model that provides a high level representation of a single,
61/// or collection, of DC files and their elements such as class imports,
62/// type definitions, structures, and Distributed Classes.
63#[derive(Debug)]
64pub struct DCFile {
65    pub(crate) imports: Vec<DCPythonImport>,
66    pub(crate) type_defs: Vec<DCTypeDefinition>,
67    pub(crate) keywords: Vec<DCKeyword>,
68    pub(crate) structs: Vec<DCStruct>,
69    pub(crate) dclasses: Vec<DClass>,
70    pub(crate) all_object_valid: bool,
71    pub(crate) inherited_fields_stale: bool,
72}
73
74impl std::fmt::Display for DCFile {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        // Print Python-style imports
77        if !self.imports.is_empty() {
78            for import in &self.imports {
79                import.fmt(f)?;
80                writeln!(f)?;
81            }
82            writeln!(f)?;
83        }
84        // Print type definitions
85        for type_def in &self.type_defs {
86            type_def.fmt(f)?;
87            writeln!(f)?;
88        }
89        if !self.keywords.is_empty() {
90            writeln!(f)?;
91        }
92        // Print Keyword definitions
93        for kw in &self.keywords {
94            kw.fmt(f)?;
95            writeln!(f)?;
96        }
97        if !self.structs.is_empty() {
98            writeln!(f)?;
99        }
100        // Print Structs
101        for strukt in &self.structs {
102            strukt.fmt(f)?;
103            writeln!(f)?;
104        }
105        if !self.dclasses.is_empty() {
106            writeln!(f)?;
107        }
108        // Print DClasses
109        for dclass in &self.dclasses {
110            dclass.fmt(f)?;
111            writeln!(f)?;
112        }
113        Ok(())
114    }
115}
116
117impl LegacyDCHash for DCFile {
118    fn generate_hash(&self, hashgen: &mut DCHashGenerator) {
119        hashgen.add_int(self.get_num_dclasses().try_into().unwrap());
120
121        for dclass in &self.dclasses {
122            dclass.generate_hash(hashgen);
123        }
124
125        hashgen.add_int(self.get_num_structs().try_into().unwrap());
126
127        for strukt in &self.structs {
128            strukt.generate_hash(hashgen);
129        }
130
131        hashgen.add_int(self.get_num_keywords().try_into().unwrap());
132
133        for kw in &self.keywords {
134            kw.generate_hash(hashgen);
135        }
136    }
137}
138
139impl DCFile {
140    /// Returns a 32-bit hash index associated with this file.  This number is
141    /// guaranteed to be consistent if the contents of the file have not changed,
142    /// and it is very likely to be different if the contents of the file do change.
143    pub fn get_legacy_hash(&self) -> globals::DCFileHash {
144        let mut hashgen: DCHashGenerator = DCHashGenerator::default();
145
146        self.generate_hash(&mut hashgen);
147        hashgen.get_hash()
148    }
149
150    /// Returns a string with the hash as a pretty format hexadecimal.
151    pub fn get_pretty_hash(&self) -> String {
152        format!("0x{:0width$x}", self.get_legacy_hash(), width = 8) // 2 hex / byte = 8 hex
153    }
154
155    // ---------- Python Imports ---------- //
156
157    pub fn get_num_imports(&self) -> usize {
158        self.imports.len()
159    }
160
161    pub fn get_python_import(&self, index: usize) -> &DCPythonImport {
162        self.imports.get(index).expect("Index out of bounds.")
163    }
164
165    // ---------- DC Keyword ---------- //
166
167    pub fn get_num_keywords(&self) -> usize {
168        self.keywords.len()
169    }
170
171    pub fn get_keyword(&self, _index: usize) -> &DCKeyword {
172        todo!();
173    }
174
175    pub fn has_keyword(&self, _keyword: String) -> bool {
176        todo!();
177    }
178
179    // ---------- Distributed Class ---------- //
180
181    pub fn get_num_dclasses(&self) -> usize {
182        self.dclasses.len()
183    }
184
185    pub fn get_dclass(&self, _index: usize) -> &DClass {
186        todo!();
187    }
188
189    pub fn get_dclass_by_id(&self, id: globals::DClassId) -> &DClass {
190        self.dclasses.get(usize::from(id)).unwrap()
191    }
192
193    pub fn get_dclass_by_name(&self, _name: &str) -> &DClass {
194        todo!();
195    }
196
197    // ---------- DC Struct ---------- //
198
199    pub fn get_num_structs(&self) -> usize {
200        self.structs.len()
201    }
202
203    pub fn get_struct(&self, _index: usize) -> &DCStruct {
204        todo!();
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn write_dc_python_import() {
214        let import: DCPythonImport = DCPythonImport {
215            module: "views".to_string(),
216            symbols: vec![],
217        };
218
219        assert_eq!(import.to_string(), "import views");
220    }
221
222    #[test]
223    fn write_dcfile_py_imports() {
224        let imports: Vec<DCPythonImport> = vec![
225            DCPythonImport {
226                module: "views".to_string(),
227                symbols: vec![],
228            },
229            DCPythonImport {
230                module: "views".to_string(),
231                symbols: vec!["DistributedDonut".to_string()],
232            },
233            DCPythonImport {
234                module: "views".to_string(),
235                symbols: vec!["Class".to_string(), "ClassAI".to_string(), "ClassOV".to_string()],
236            },
237        ];
238
239        let dcf: DCFile = DCFile {
240            structs: vec![],
241            dclasses: vec![],
242            imports,
243            keywords: vec![],
244            type_defs: vec![],
245            all_object_valid: false,
246            inherited_fields_stale: false,
247        };
248
249        assert_eq!(
250            dcf.to_string(),
251            "\
252            import views\n\
253            from views import DistributedDonut\n\
254            from views import Class, ClassAI, ClassOV\n\
255            \n\
256            ",
257        );
258    }
259}
260
261pub(crate) mod semantics {
262    use super::ast;
263    use crate::parser::error::{Diagnostic, SemanticError};
264    use crate::parser::pipeline::{PipelineData, TopLevelSymbol};
265    use std::collections::HashSet;
266
267    /// Redundancy check for an array of strings that represent view suffixes.
268    /// The lexer already generates a specific token type for view suffixes,
269    /// and the parser grammar expects this token type, so we already are
270    /// guaranteed that the view suffixes are valid.
271    fn analyze_view_suffixes(pipeline: &mut PipelineData, view_suffixes: &ast::ViewSuffixes) {
272        let mut recorded_suffixes: HashSet<String> = HashSet::default();
273
274        for view_suffix in view_suffixes {
275            if !recorded_suffixes.insert(view_suffix.view.clone()) {
276                let diag: Diagnostic = Diagnostic::error(
277                    view_suffix.span,
278                    pipeline,
279                    SemanticError::RedundantViewSuffix(view_suffix.view.clone()),
280                );
281
282                pipeline
283                    .emit_diagnostic(diag.into())
284                    .expect("Failed to emit diagnostic.");
285            }
286        }
287    }
288
289    pub fn analyze_python_import(pipeline: &mut PipelineData, import: &ast::PythonImport) {
290        // check view suffixes
291        analyze_view_suffixes(pipeline, &import.module.symbol_views);
292        analyze_view_suffixes(pipeline, &import.class.symbol_views);
293    }
294
295    pub fn analyze_keyword(pipeline: &mut PipelineData, keyword: &ast::KeywordDefinition) {
296        let already_defined: bool = pipeline
297            .dc_data
298            .symbol_exists(&keyword.identifier, TopLevelSymbol::KeywordDef);
299
300        if already_defined {
301            let diag: Diagnostic = Diagnostic::error(
302                keyword.span,
303                pipeline,
304                SemanticError::AlreadyDefined(keyword.identifier.clone()),
305            );
306
307            pipeline
308                .emit_diagnostic(diag.into())
309                .expect("Failed to emit diagnostic.");
310            return;
311        }
312        // add this keyword definition to our symbol map
313        pipeline
314            .dc_data
315            .register_symbol(keyword.identifier.clone(), TopLevelSymbol::KeywordDef);
316    }
317
318    pub fn analyze_typedef(pipeline: &mut PipelineData, typedef: &ast::TypeDefinition) {
319        //let new_td: DCTypeDefinition = typedef.into();
320        // TODO: semantic checks (e.g. typedef Struct Name; -> verify Struct exists)
321        //self.typedefs.push(new_td);
322    }
323
324    pub fn analyze_dclass(pipeline: &mut PipelineData, dclass: &ast::DClass) {
325        //self.dclasses.push(dclass);
326    }
327
328    pub fn analyze_struct(pipeline: &mut PipelineData, strukt: &ast::Struct) {
329        //let new_struct: DCStruct = strukt.into();
330        //self.structs.push(new_struct);
331    }
332}
333
334pub(crate) mod generation {
335    use super::DCPythonImport;
336    use crate::parser::ast;
337
338    /// 'Untangles' a [`ast::PythonImport`], which represents a python import line,
339    /// into one or more [`DCPythonImport`] structures, which represent symbol imports
340    /// from a python module (with view suffixes applied) and adds them to the DC tree.
341    ///
342    pub fn add_python_import(import: &ast::PythonImport) -> Vec<DCPythonImport> {
343        let mut imports: Vec<DCPythonImport> = vec![];
344        let mut class_symbols: Vec<String> = vec![import.class.symbol.clone()];
345
346        // Separates "Class/AI/OV" to ["Class", "ClassAI", "ClassOV"]
347        if !import.class.symbol_views.is_empty() {
348            for class_suffix in &import.class.symbol_views {
349                class_symbols.push(import.class.symbol.clone() + &class_suffix.view);
350            }
351        }
352
353        // Handles e.g. "from module/AI/OV/UD import DistributedThing/AI/OV/UD"
354        if !import.module.symbol_views.is_empty() {
355            let mut c_symbol: String = class_symbols.first().unwrap().clone();
356
357            imports.push(DCPythonImport {
358                module: import.module.symbol.clone(),
359                symbols: vec![c_symbol],
360            });
361
362            for (i, module_suffix) in import.module.symbol_views.iter().enumerate() {
363                let full_import: String = import.module.symbol.clone() + &module_suffix.view;
364
365                if (class_symbols.len() - 1) <= i {
366                    c_symbol = class_symbols.last().unwrap().clone();
367                } else {
368                    c_symbol = class_symbols.get(i + 1).unwrap().clone();
369                }
370
371                imports.push(DCPythonImport {
372                    module: full_import,
373                    symbols: vec![c_symbol],
374                });
375            }
376        } else {
377            // No view suffixes for the module symbol, so just push the symbol.
378            imports.push(DCPythonImport {
379                module: import.module.symbol.clone(),
380                symbols: class_symbols,
381            });
382        }
383
384        imports
385    }
386}