Skip to main content

aam_core/pipeline/
mod.rs

1//! The new five-stage architecture pipeline for AAML parsing.
2//!
3//! Pipeline stages:
4//! 1. **Lexer** - tokenizes raw text into a ` Token ` stream
5//! 2. **Parser** - builds AST from tokens, manages scope
6//! 3. **Validator** - applies schema and type checks to AST
7//! 4. **Executer** - executes directives and populates the final map
8//! 5. **Output** - final key-value map, schemas, types
9
10#![allow(clippy::missing_errors_doc)]
11
12pub mod execution_descriptor;
13pub mod formatter;
14pub mod lexer;
15pub mod parser;
16pub mod scope_manager;
17pub mod utils;
18
19pub(crate) mod source_dir;
20
21pub mod executer;
22pub mod executor_traits;
23pub mod tasks;
24pub mod validator;
25
26pub use executer::{DefaultExecuter, Executer};
27pub use execution_descriptor::{
28    CommandInfo, ExecutionContext, ExecutionDescriptor, SchemaInfo, TypeInfo,
29};
30pub use executor_traits::{
31    DefaultParserExecutor, DefaultValidateExecutor, ParserExecutor, ValidateExecutor,
32};
33pub use formatter::{DefaultFormatter, FormatRange, Formatter, FormattingOptions};
34pub use lexer::{DefaultLexer, Lexer, Token};
35pub use parser::{AstNode, DefaultParser, Parser};
36pub use scope_manager::ScopeManager;
37pub use tasks::{ExecutionTask, ParseTask, TaskError, TaskExecutionResult, ValidationTask};
38pub use validator::{DefaultValidator, Validator};
39
40use crate::error::AamlError;
41use bumpalo::Bump;
42use smol_str::SmolStr;
43use std::collections::HashSet;
44use tinyvec::TinyVec;
45
46// Hash backends are selected by cfg precedence below so `--all-features` remains compilable.
47
48#[cfg(feature = "hash-ripemd")]
49#[derive(Default, Clone)]
50pub struct RipemdBuildHasher;
51
52#[cfg(feature = "hash-ripemd")]
53#[derive(Default, Clone)]
54pub struct RipemdHasher {
55    bytes: Vec<u8>,
56}
57
58#[cfg(feature = "hash-ripemd")]
59impl std::hash::BuildHasher for RipemdBuildHasher {
60    type Hasher = RipemdHasher;
61
62    fn build_hasher(&self) -> Self::Hasher {
63        RipemdHasher::default()
64    }
65}
66
67#[cfg(feature = "hash-ripemd")]
68impl std::hash::Hasher for RipemdHasher {
69    fn finish(&self) -> u64 {
70        use ripemd::Digest;
71        let mut hasher = ripemd::Ripemd160::new();
72        hasher.update(&self.bytes);
73        let digest = hasher.finalize();
74
75        let mut out = [0_u8; 8];
76        out.copy_from_slice(&digest[..8]);
77        u64::from_le_bytes(out)
78    }
79
80    fn write(&mut self, bytes: &[u8]) {
81        self.bytes.extend_from_slice(bytes);
82    }
83}
84
85#[cfg(feature = "hash-std")]
86pub type PipelineBuildHasher = std::collections::hash_map::RandomState;
87
88#[cfg(all(not(feature = "hash-std"), feature = "hash-fx"))]
89pub type PipelineBuildHasher = rustc_hash::FxBuildHasher;
90
91#[cfg(all(
92    not(feature = "hash-std"),
93    not(feature = "hash-fx"),
94    feature = "hash-ahash"
95))]
96pub type PipelineBuildHasher = ahash::RandomState;
97
98#[cfg(all(
99    not(feature = "hash-std"),
100    not(feature = "hash-fx"),
101    not(feature = "hash-ahash"),
102    feature = "hash-rapidhash"
103))]
104pub type PipelineBuildHasher = rapidhash::fast::RandomState;
105
106#[cfg(all(
107    not(feature = "hash-std"),
108    not(feature = "hash-fx"),
109    not(feature = "hash-ahash"),
110    not(feature = "hash-rapidhash"),
111    feature = "hash-ripemd"
112))]
113pub type PipelineBuildHasher = RipemdBuildHasher;
114
115#[cfg(not(any(
116    feature = "hash-std",
117    feature = "hash-fx",
118    feature = "hash-ahash",
119    feature = "hash-rapidhash",
120    feature = "hash-ripemd"
121)))]
122pub type PipelineBuildHasher = std::collections::hash_map::RandomState;
123
124pub type PipelineHashMap<K, V> = std::collections::HashMap<K, V, PipelineBuildHasher>;
125
126#[inline]
127pub(crate) fn new_pipeline_hash_map<K, V>() -> PipelineHashMap<K, V> {
128    PipelineHashMap::with_hasher(PipelineBuildHasher::default())
129}
130
131type AamlString = SmolStr;
132type ErrorAccumulator = TinyVec<[Option<AamlError>; 4]>;
133
134/// Output produced by the full pipeline after all stages complete successfully.
135pub struct PipelineOutput {
136    pub map: PipelineHashMap<AamlString, AamlString>,
137    pub schemas: PipelineHashMap<AamlString, SchemaInfo>,
138    pub types: PipelineHashMap<AamlString, TypeInfo>,
139}
140
141impl std::fmt::Debug for PipelineOutput {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        f.debug_struct("PipelineOutput")
144            .field("map", &self.map)
145            .field("schemas", &self.schemas)
146            .field("types_count", &self.types.len())
147            .finish()
148    }
149}
150
151impl PipelineOutput {
152    #[must_use]
153    pub fn new() -> Self {
154        Self {
155            map: new_pipeline_hash_map(),
156            schemas: new_pipeline_hash_map(),
157            types: new_pipeline_hash_map(),
158        }
159    }
160}
161
162impl Default for PipelineOutput {
163    fn default() -> Self {
164        Self::new()
165    }
166}
167
168pub struct Pipeline {
169    lexer: Box<dyn Lexer>,
170    parser: Box<dyn Parser>,
171    validator: Box<dyn Validator>,
172    validate_executor: Box<dyn ValidateExecutor>,
173    parser_executor: Box<dyn ParserExecutor>,
174    executer: Box<dyn Executer>,
175    formatter: Box<dyn Formatter>,
176}
177
178impl Pipeline {
179    /// Processes AAML content using the 5-stage arena-based pipeline.
180    pub fn process(&self, content: &str) -> Result<PipelineOutput, Vec<AamlError>> {
181        let arena = Bump::new();
182        self.process_with_arena(content, &arena)
183    }
184
185    /// Processes AAML content with a source directory for relative path resolution.
186    pub fn process_with_source_dir(
187        &self,
188        content: &str,
189        source_dir: Option<&std::path::Path>,
190    ) -> Result<PipelineOutput, Vec<AamlError>> {
191        let arena = Bump::new();
192        self.process_with_arena_and_source_dir(content, &arena, source_dir)
193    }
194
195    /// Creates a pipeline instance.
196    ///
197    /// Enable the `parallel` feature to run stateless parse-task pre-validation in parallel.
198    #[must_use]
199    pub fn new() -> Self {
200        Self {
201            lexer: Box::new(DefaultLexer::new()),
202            parser: Box::new(DefaultParser::new()),
203            validator: Box::new(DefaultValidator::new()),
204            validate_executor: Box::new(DefaultValidateExecutor::new()),
205            parser_executor: Box::new(DefaultParserExecutor::new()),
206            executer: Box::new(DefaultExecuter::new()),
207            formatter: Box::new(DefaultFormatter::new()),
208        }
209    }
210
211    #[cfg(feature = "parallel")]
212    fn execute_parse_tasks_parallel(tasks: &[ParseTask<'_>]) -> Vec<TaskError> {
213        use rayon::prelude::*;
214
215        tasks
216            .par_iter()
217            .filter_map(|task| task.validate_stateless().err())
218            .collect()
219    }
220
221    fn collect_parse_errors(all_errors: &mut ErrorAccumulator, errors: Vec<TaskError>) {
222        for err in errors {
223            all_errors.push(Some(AamlError::ParseError {
224                line: err.line,
225                content: String::new(),
226                details: err.message,
227                diagnostics: None,
228            }));
229        }
230    }
231
232    fn collect_validation_errors(all_errors: &mut ErrorAccumulator, errors: Vec<TaskError>) {
233        for err in errors {
234            all_errors.push(Some(AamlError::DirectiveError {
235                directive: "validation".to_string(),
236                message: err.message,
237                diagnostics: None,
238            }));
239        }
240    }
241
242    fn parse_ast<'a>(
243        &self,
244        tokens: &[Token<'a>],
245        all_errors: &mut ErrorAccumulator,
246    ) -> Vec<AstNode<'a>> {
247        let parse_output = self.parser.parse_with_recovery(tokens);
248        for err in parse_output.errors {
249            all_errors.push(Some(err));
250        }
251        parse_output.ast
252    }
253
254    fn run_parse_tasks<'a>(
255        &self,
256        ast: &[AstNode<'a>],
257        arena: &'a Bump,
258        descriptor: &mut ExecutionDescriptor<'a>,
259        all_errors: &mut ErrorAccumulator,
260    ) {
261        let parse_tasks = self.parser.generate_parse_tasks(ast);
262        descriptor.add_parse_tasks(parse_tasks.clone());
263
264        #[cfg(feature = "parallel")]
265        let stateless_errors = Self::execute_parse_tasks_parallel(&parse_tasks);
266
267        #[cfg(feature = "parallel")]
268        {
269            if !stateless_errors.is_empty() {
270                Self::collect_parse_errors(all_errors, stateless_errors);
271            }
272        }
273
274        #[cfg(feature = "parallel")]
275        let sequential_tasks: Vec<ParseTask<'a>> = parse_tasks
276            .into_iter()
277            .filter(|task| task.validate_stateless().is_ok())
278            .collect();
279
280        #[cfg(not(feature = "parallel"))]
281        let sequential_tasks = parse_tasks;
282
283        let parse_result =
284            self.parser_executor
285                .execute_batch(&sequential_tasks, arena, descriptor.context_mut());
286        if !parse_result.success {
287            Self::collect_parse_errors(all_errors, parse_result.errors);
288        }
289    }
290
291    fn run_validation_tasks<'a>(
292        &self,
293        ast: &'a [AstNode<'a>],
294        descriptor: &mut ExecutionDescriptor<'a>,
295        all_errors: &mut ErrorAccumulator,
296    ) {
297        let validation_tasks = self.validator.validate(ast).unwrap_or_else(|e| {
298            all_errors.push(Some(e));
299            Vec::new()
300        });
301        descriptor.add_validation_tasks(validation_tasks.clone());
302
303        let validation_result = self
304            .validate_executor
305            .execute_batch(&validation_tasks, descriptor.context());
306        if !validation_result.success {
307            Self::collect_validation_errors(all_errors, validation_result.errors);
308        }
309
310        for err in Self::validate_registered_types(descriptor.context()) {
311            all_errors.push(Some(err));
312        }
313
314        for err in Self::validate_schema_declared_types(descriptor.context()) {
315            all_errors.push(Some(err));
316        }
317
318        for err in Self::validate_schema_required_fields(descriptor.context()) {
319            all_errors.push(Some(err));
320        }
321
322        for err in Self::validate_schema_field_types(descriptor.context()) {
323            all_errors.push(Some(err));
324        }
325    }
326
327    fn validate_type_reference(
328        type_name: &str,
329        context: &ExecutionContext,
330        trail: &mut Vec<String>,
331    ) -> Result<(), AamlError> {
332        if let Some(inner) = crate::types_aam::list::ListType::parse_inner(type_name) {
333            return Self::validate_type_reference(inner.trim(), context, trail);
334        }
335
336        if crate::types_aam::resolve_builtin(type_name).is_ok()
337            || context.schemas.contains_key(type_name)
338        {
339            return Ok(());
340        }
341
342        let Some(type_info) = context.types.get(type_name) else {
343            return Err(AamlError::InvalidType {
344                type_name: type_name.to_string(),
345                details: format!("Unknown type '{type_name}'"),
346                provided: String::new(),
347                diagnostics: None,
348            });
349        };
350
351        if type_info.spec == "schema" {
352            return Ok(());
353        }
354
355        if let Some(cycle_start) = trail.iter().position(|n| n == type_name) {
356            let mut cycle = trail[cycle_start..].to_vec();
357            cycle.push(type_name.to_string());
358            return Err(AamlError::CircularDependency {
359                path: cycle.join(" -> "),
360                diagnostics: None,
361            });
362        }
363
364        if type_info.spec == type_name {
365            return Err(AamlError::InvalidType {
366                type_name: type_name.to_string(),
367                details: format!("Unknown type '{type_name}'"),
368                provided: String::new(),
369                diagnostics: None,
370            });
371        }
372
373        trail.push(type_name.to_string());
374        let result = Self::validate_type_reference(type_info.spec.as_str(), context, trail);
375        trail.pop();
376        result
377    }
378
379    fn validate_registered_types(context: &ExecutionContext) -> Vec<AamlError> {
380        context
381            .types
382            .iter()
383            .filter(|(_, type_info)| type_info.line > 0 && type_info.spec != "schema")
384            .filter_map(|(_, type_info)| {
385                let mut trail = Vec::new();
386                Self::validate_type_reference(type_info.spec.as_str(), context, &mut trail)
387                    .err()
388                    .map(|err| match err {
389                        AamlError::CircularDependency { .. } => err,
390                        _ => AamlError::InvalidType {
391                            type_name: type_info.name.to_string(),
392                            details: format!(
393                                "Type '{}' references unknown definition '{}'",
394                                type_info.name, type_info.spec
395                            ),
396                            provided: err.short_message(),
397                            diagnostics: None,
398                        },
399                    })
400            })
401            .collect()
402    }
403
404    fn validate_schema_declared_types(context: &ExecutionContext) -> Vec<AamlError> {
405        context
406            .schemas
407            .iter()
408            .flat_map(|(schema_name, schema)| {
409                schema
410                    .fields
411                    .iter()
412                    .filter_map(move |(field, (type_name, _))| {
413                        let mut trail = Vec::new();
414                        Self::validate_type_reference(type_name, context, &mut trail)
415                            .err()
416                            .map(|_| AamlError::SchemaValidationError {
417                                schema: schema_name.to_string(),
418                                field: field.to_string(),
419                                type_name: type_name.to_string(),
420                                details: format!(
421                                    "Unknown type '{type_name}' declared for field '{field}'"
422                                ),
423                                diagnostics: None,
424                            })
425                    })
426            })
427            .collect()
428    }
429
430    fn collect_referenced_schemas(context: &ExecutionContext) -> HashSet<SmolStr> {
431        context
432            .schemas
433            .values()
434            .flat_map(|schema| schema.fields.values())
435            .filter_map(|(type_name, _)| {
436                if context.schemas.contains_key(type_name.as_str()) {
437                    Some(type_name.clone())
438                } else {
439                    None
440                }
441            })
442            .collect()
443    }
444
445    fn validate_schema_required_fields(context: &ExecutionContext) -> Vec<AamlError> {
446        let referenced_schemas = Self::collect_referenced_schemas(context);
447
448        context
449            .schemas
450            .iter()
451            .filter(|(schema_name, _)| !referenced_schemas.contains(schema_name.as_str()))
452            .filter(|(_, schema)| {
453                schema
454                    .fields
455                    .keys()
456                    .any(|f| context.map.contains_key(f.as_str()))
457            })
458            .flat_map(|(schema_name, schema)| {
459                schema
460                    .fields
461                    .iter()
462                    .filter_map(move |(field, (type_name, is_optional))| {
463                        if *is_optional || context.map.contains_key(field.as_str()) {
464                            return None;
465                        }
466
467                        let mut seen_aliases = HashSet::new();
468                        if Self::is_schema_type_reference(type_name, context, &mut seen_aliases) {
469                            return None;
470                        }
471
472                        Some(AamlError::SchemaValidationError {
473                            schema: schema_name.to_string(),
474                            field: field.to_string(),
475                            type_name: type_name.to_string(),
476                            details: format!("Missing required field '{field}'"),
477                            diagnostics: None,
478                        })
479                    })
480            })
481            .collect()
482    }
483
484    fn is_schema_type_reference(
485        type_name: &str,
486        context: &ExecutionContext,
487        seen_aliases: &mut HashSet<String>,
488    ) -> bool {
489        if context.schemas.contains_key(type_name) {
490            return true;
491        }
492
493        let Some(type_info) = context.types.get(type_name) else {
494            return false;
495        };
496
497        if type_info.spec == "schema" {
498            return true;
499        }
500
501        if type_info.spec == type_name || !seen_aliases.insert(type_name.to_string()) {
502            return false;
503        }
504
505        Self::is_schema_type_reference(type_info.spec.as_str(), context, seen_aliases)
506    }
507
508    fn validate_schema_field_types(context: &ExecutionContext) -> Vec<AamlError> {
509        context
510            .map
511            .iter()
512            .filter_map(|(key, value)| {
513                let (schema_name, (type_name, _)) =
514                    context.schemas.iter().find_map(|(schema_name, schema)| {
515                        schema
516                            .fields
517                            .get(key.as_str())
518                            .map(|field_info| (schema_name, field_info))
519                    })?;
520
521                utils::validate_type_value(value, type_name, context)
522                    .err()
523                    .map(|e| AamlError::SchemaValidationError {
524                        schema: schema_name.to_string(),
525                        field: key.to_string(),
526                        type_name: type_name.to_string(),
527                        details: format!(
528                            "Type mismatch for field '{}': {}",
529                            key,
530                            e.short_message()
531                        ),
532                        diagnostics: None,
533                    })
534            })
535            .collect()
536    }
537
538    fn process_with_tasks<'a>(
539        &self,
540        content: &'a str,
541        arena: &'a Bump,
542        source_dir: Option<&std::path::Path>,
543    ) -> Result<PipelineOutput, Vec<AamlError>> {
544        let mut all_errors: ErrorAccumulator = TinyVec::new();
545
546        let tokens = match self.lexer.tokenize(content) {
547            Ok(t) => t,
548            Err(e) => return Err(vec![e]),
549        };
550
551        let ast = self.parse_ast(&tokens, &mut all_errors);
552        let mut descriptor = ExecutionDescriptor::new(ast.clone(), "inline".to_string());
553
554        // RAII guard: resets thread-local source_dir on drop (including panic)
555        let _guard = source_dir::SourceDirGuard::new(source_dir.map(std::path::Path::to_path_buf));
556
557        self.run_parse_tasks(&ast, arena, &mut descriptor, &mut all_errors);
558        self.run_validation_tasks(&ast, &mut descriptor, &mut all_errors);
559
560        if let Some(errors) = finalize_error_accumulator(all_errors) {
561            return Err(errors);
562        }
563
564        descriptor.add_execution_tasks(self.parser.generate_execution_tasks(&ast));
565
566        if let Err(e) = self.executer.execute(&mut descriptor) {
567            return Err(vec![e]);
568        }
569
570        Ok(PipelineOutput {
571            map: descriptor.context.map,
572            schemas: descriptor.context.schemas,
573            types: descriptor.context.types,
574        })
575    }
576
577    pub fn process_with_arena<'a>(
578        &self,
579        content: &'a str,
580        arena: &'a Bump,
581    ) -> Result<PipelineOutput, Vec<AamlError>> {
582        self.process_with_tasks(content, arena, None)
583    }
584
585    pub fn process_with_arena_and_source_dir<'a>(
586        &self,
587        content: &'a str,
588        arena: &'a Bump,
589        source_dir: Option<&std::path::Path>,
590    ) -> Result<PipelineOutput, Vec<AamlError>> {
591        self.process_with_tasks(content, arena, source_dir)
592    }
593
594    pub fn format(
595        &self,
596        nodes: &[AstNode],
597        options: &FormattingOptions,
598    ) -> Result<String, AamlError> {
599        self.formatter.format_document(nodes, options)
600    }
601
602    pub fn format_range(
603        &self,
604        nodes: &[AstNode],
605        range: FormatRange,
606        options: &FormattingOptions,
607    ) -> Result<String, AamlError> {
608        self.formatter.format_range(nodes, range, options)
609    }
610}
611
612impl Default for Pipeline {
613    fn default() -> Self {
614        Self::new()
615    }
616}
617
618#[inline]
619fn finalize_error_accumulator(errors: ErrorAccumulator) -> Option<Vec<AamlError>> {
620    if errors.is_empty() {
621        return None;
622    }
623    Some(errors.into_iter().flatten().collect())
624}