donet_core/parser/
pipeline.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//! Defines the [`PipelineStage`] structure, which manages
21//! data stored in memory throughout the DC parser pipeline.
22
23use super::ast;
24use super::error::Diagnostic as DCDiagnostic;
25use super::error::SemanticError;
26use super::lexer::Span;
27use crate::globals;
28use anyhow::{anyhow, Result};
29use codespan_reporting::diagnostic::Diagnostic;
30use codespan_reporting::diagnostic::Severity;
31use codespan_reporting::files::{self, SimpleFiles};
32use codespan_reporting::term;
33use multimap::MultiMap;
34use term::termcolor::{ColorChoice, StandardStream};
35
36/// Used by the [`PipelineData`] structure to keep track
37/// of the current pipeline stage to properly store its state.
38#[derive(Debug, Default, Clone, PartialEq, Eq)]
39pub(crate) enum PipelineStage {
40    #[default]
41    Parser, // includes lexical and syntax analysis
42    SemanticAnalyzer,
43    Generation,
44}
45
46impl PipelineStage {
47    pub(crate) fn next(&self) -> Self {
48        match self {
49            PipelineStage::Parser => PipelineStage::SemanticAnalyzer,
50            PipelineStage::SemanticAnalyzer => PipelineStage::Generation,
51            PipelineStage::Generation => panic!("No next stage in pipeline."),
52        }
53    }
54}
55
56#[derive(PartialEq)]
57pub enum TopLevelSymbol {
58    TypeDef,
59    KeywordDef,
60    Struct,
61    DClass,
62}
63
64/// Data structure used to keep track of declarations'
65/// symbols (their identifiers) for usage during
66/// semantic analysis.
67pub type SymbolMap = MultiMap<String, TopLevelSymbol>;
68
69/// Globals that the semantic analyzer uses while
70/// parsing the abstract syntax tree.
71#[derive(Default)]
72pub struct DCData {
73    symbol_map: SymbolMap,
74    next_dclass_id: globals::DClassId,
75    next_field_id: globals::FieldId,
76}
77
78impl DCData {
79    /// Inserts new key/value pair to the private symbol map.
80    pub fn register_symbol(&mut self, identifier: String, symbol_type: TopLevelSymbol) {
81        self.symbol_map.insert(identifier, symbol_type);
82    }
83
84    /// Check is a given symbol exists in our symbol map (a.k.a
85    /// it is a registered type declaration in the DC file.)
86    pub fn symbol_exists(&self, identifier: &String, symbol_type: TopLevelSymbol) -> bool {
87        self.symbol_map
88            .iter()
89            .find(|&x| x == (&identifier, &symbol_type))
90            .is_some()
91    }
92
93    /// Gets the next dclass ID based on the current allocated IDs.
94    ///
95    /// If an error is returned, this DC file has run out of dclass
96    /// IDs to assign. This function will emit the error diagnostic.
97    ///
98    pub fn get_next_dclass_id(
99        &mut self,
100        pipeline: &mut PipelineData,
101        dclass: ast::DClass, // current dclass ref for diagnostic span
102    ) -> Result<globals::DClassId> {
103        let next_id: globals::DClassId = self.next_dclass_id;
104
105        if next_id == globals::DClassId::MAX {
106            // We have reached the maximum number of dclass declarations.
107            let diag: DCDiagnostic =
108                DCDiagnostic::error(dclass.span, pipeline, SemanticError::DClassOverflow);
109
110            pipeline
111                .emit_diagnostic(diag.into())
112                .expect("Failed to emit diagnostic.");
113
114            return Err(anyhow!("Ran out of 16-bit DClass IDs!"));
115        }
116
117        self.next_dclass_id += 1; // increment
118        Ok(next_id)
119    }
120
121    /// Gets the next field ID based on the current allocated IDs.
122    ///
123    /// If an error is returned, this DC file has run out of field
124    /// IDs to assign. This function will emit the error diagnostic.
125    ///
126    pub fn get_next_field_id(
127        &mut self,
128        pipeline: &mut PipelineData,
129        field_span: Span,
130    ) -> Result<globals::FieldId> {
131        let next_id: globals::FieldId = self.next_field_id;
132
133        if next_id == globals::DClassId::MAX {
134            // We have reached the maximum number of dclass declarations.
135            let diag: DCDiagnostic = DCDiagnostic::error(field_span, pipeline, SemanticError::DClassOverflow);
136
137            pipeline
138                .emit_diagnostic(diag.into())
139                .expect("Failed to emit diagnostic.");
140
141            return Err(anyhow!("Ran out of 16-bit Field IDs!"));
142        }
143
144        self.next_field_id += 1; // increment
145        Ok(next_id)
146    }
147}
148
149/// Data stored in memory throughout the DC parser pipeline.
150///
151/// Sets up writer and codespan config for rendering diagnostics
152/// to stderr & storing DC files that implement codespan's File trait.
153pub(crate) struct PipelineData<'a> {
154    stage: PipelineStage,
155    _writer: StandardStream,
156    _config: term::Config,
157    diagnostics_enabled: bool,
158    errors_emitted: usize,
159    pub files: SimpleFiles<&'a str, &'a str>,
160    current_file: usize,
161    pub syntax_trees: Vec<ast::Root>,
162    pub dc_data: DCData,
163}
164
165/// If the [`PipelineData`] structure is dropped, this means the
166/// pipeline finished, either with success or error.
167///
168/// Upon drop, emit a final diagnostic with the finish status of the pipeline.
169impl Drop for PipelineData<'_> {
170    fn drop(&mut self) {
171        if self.errors_emitted > 0 {
172            let diag = Diagnostic::error().with_message(format!(
173                "Failed to read DC files due to {} previous errors.",
174                self.errors_emitted
175            ));
176
177            self.emit_diagnostic(diag).expect("Failed to emit diagnostic.");
178        }
179    }
180}
181
182impl Default for PipelineData<'_> {
183    fn default() -> Self {
184        Self {
185            stage: PipelineStage::default(),
186            _writer: StandardStream::stderr(ColorChoice::Always),
187            _config: term::Config::default(),
188            diagnostics_enabled: {
189                // Disable diagnostics in unit tests
190                cfg_if! {
191                    if #[cfg(test)] {
192                        false
193                    } else {
194                        true
195                    }
196                }
197            },
198            errors_emitted: 0,
199            files: SimpleFiles::new(),
200            current_file: 0,
201            syntax_trees: vec![],
202            dc_data: DCData::default(),
203        }
204    }
205}
206
207impl PipelineData<'_> {
208    /// Thin wrapper for emitting a codespan diagnostic using `PipelineData` properties.
209    pub(crate) fn emit_diagnostic(&mut self, diag: Diagnostic<usize>) -> Result<(), files::Error> {
210        if diag.severity == Severity::Error {
211            self.errors_emitted += 1;
212        }
213        if !self.diagnostics_enabled {
214            return Ok(());
215        }
216        term::emit(&mut self._writer.lock(), &self._config, &self.files, &diag)
217    }
218
219    #[inline(always)]
220    pub(crate) fn current_stage(&self) -> PipelineStage {
221        self.stage.clone()
222    }
223
224    pub(crate) fn next_stage(&mut self) {
225        self.stage = self.stage.next();
226        self.current_file = 0;
227    }
228
229    #[inline(always)]
230    pub(crate) fn current_file(&self) -> usize {
231        self.current_file
232    }
233
234    pub(crate) fn next_file(&mut self) {
235        self.current_file += 1
236    }
237
238    #[inline(always)]
239    pub(crate) fn failing(&self) -> bool {
240        self.errors_emitted > 0
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn next_stage_state() {
250        let mut pipeline: PipelineData = PipelineData::default();
251
252        assert_eq!(pipeline.stage, PipelineStage::Parser);
253
254        pipeline.next_file(); // increase file counter to 1
255
256        pipeline.next_stage(); // should reset state for next stage
257
258        assert_eq!(pipeline.stage, PipelineStage::SemanticAnalyzer);
259        assert_eq!(pipeline.current_file, 0);
260
261        pipeline.next_stage();
262        assert_eq!(pipeline.stage, PipelineStage::Generation);
263    }
264}