Skip to main content

aam_core/pipeline/
execution_descriptor.rs

1//! Enhanced `ExecutionDescriptor` serving as the comprehensive execution manifest.
2//!
3//! The `ExecutionDescriptor` replaces direct AAML struct dependency in the Executer.
4//! It bundles all necessary context, tasks, and metadata for clean execution.
5
6use crate::error::AamlError;
7use crate::pipeline::parser::AstNode;
8use crate::pipeline::tasks::{ExecutionStats, ExecutionTask, ParseTask, ValidationTask};
9use crate::pipeline::{PipelineBuildHasher, PipelineHashMap};
10use smol_str::SmolStr;
11use std::collections::HashSet;
12
13#[inline]
14fn new_pipeline_map<K, V>() -> PipelineHashMap<K, V> {
15    PipelineHashMap::with_hasher(PipelineBuildHasher::default())
16}
17
18/// A comprehensive execution manifest that completely replaces AAML struct usage.
19///
20/// `ExecutionDescriptor` aggregates all necessary information for the Executer to
21/// materialize the configuration without requiring the legacy AAML struct.
22#[derive(Debug)]
23pub struct ExecutionDescriptor<'a> {
24    /// Original line numbers from source, indexed by AST node
25    pub line_numbers: Vec<usize>,
26
27    /// Execution context containing parsed configuration data
28    pub context: ExecutionContext<'a>,
29
30    /// Original parsed AST nodes for reference and diagnostics
31    pub parsed_outputs: Vec<AstNode<'a>>,
32
33    /// Lazy tasks for the parsing phase
34    pub parse_tasks: Vec<ParseTask<'a>>,
35
36    /// Lazy tasks for the validation phase
37    pub validation_tasks: Vec<ValidationTask<'a>>,
38
39    /// Lazy tasks for the execution phase
40    pub execution_tasks: Vec<ExecutionTask<'a>>,
41
42    /// Execution statistics
43    pub stats: ExecutionStats,
44}
45
46/// Encapsulates all runtime context needed during execution.
47///
48/// This struct holds the accumulated state that would normally be scattered
49/// across the AAML struct and various registries.
50#[derive(Debug)]
51pub struct ExecutionContext<'a> {
52    /// Source file path or identifier (for error reporting)
53    pub source: std::borrow::Cow<'a, str>,
54
55    /// Key-value map accumulated during parsing (using `SmolStr` for SSO)
56    pub map: PipelineHashMap<SmolStr, SmolStr>,
57
58    /// Schema definitions accumulated from @schema directives
59    pub schemas: PipelineHashMap<SmolStr, SchemaInfo>,
60
61    /// Type definitions accumulated from @type directives
62    pub types: PipelineHashMap<SmolStr, TypeInfo>,
63
64    /// Registered commands (directives)
65    pub commands: PipelineHashMap<std::borrow::Cow<'a, str>, CommandInfo<'a>>,
66
67    /// Line number map: key → line number where it was defined
68    pub key_line_map: PipelineHashMap<SmolStr, usize>,
69
70    /// Scope tracking for nested configurations
71    pub scope_stack: Vec<std::borrow::Cow<'a, str>>,
72
73    /// Circular reference detection set
74    pub visited_keys: HashSet<SmolStr>,
75
76    /// Import cache to prevent re-importing the same file
77    pub imported_files: HashSet<std::borrow::Cow<'a, str>>,
78}
79
80/// Information about a registered schema.
81#[derive(Debug, Clone)]
82pub struct SchemaInfo {
83    /// Schema name
84    pub name: SmolStr,
85
86    /// Field name → (`type_name`, `is_optional`)
87    pub fields: PipelineHashMap<SmolStr, (SmolStr, bool)>,
88
89    /// Line number where schema was defined
90    pub line: usize,
91}
92
93/// Information about a registered type.
94#[derive(Debug, Clone)]
95pub struct TypeInfo {
96    /// Type name
97    pub name: SmolStr,
98
99    /// Type specification (e.g., "i32", "list<string>", "vector2")
100    pub spec: SmolStr,
101
102    /// Custom validation rules (if any)
103    pub validator: Option<SmolStr>,
104
105    /// Default value used by inheritance when a required field is missing.
106    pub default_value: Option<SmolStr>,
107
108    /// Optional metadata bag for pipeline/type adapters.
109    pub metadata: PipelineHashMap<SmolStr, SmolStr>,
110
111    /// Line number where type was defined
112    pub line: usize,
113}
114
115/// Information about a registered command (directive).
116#[derive(Debug, Clone)]
117pub struct CommandInfo<'a> {
118    /// Command name
119    pub name: std::borrow::Cow<'a, str>,
120
121    /// Expected argument pattern
122    pub arg_pattern: std::borrow::Cow<'a, str>,
123
124    /// Line number where command was registered
125    pub line: usize,
126}
127
128impl<'a> ExecutionContext<'a> {
129    /// Creates a new empty execution context.
130    pub fn new(source: impl Into<std::borrow::Cow<'a, str>>) -> Self {
131        let mut context = Self {
132            source: source.into(),
133            map: new_pipeline_map(),
134            schemas: new_pipeline_map(),
135            types: new_pipeline_map(),
136            commands: new_pipeline_map(),
137            key_line_map: new_pipeline_map(),
138            scope_stack: vec!["root".into()],
139            visited_keys: HashSet::default(),
140            imported_files: HashSet::default(),
141        };
142
143        context.register_builtin_type_defaults();
144        context
145    }
146
147    fn register_builtin_type_defaults(&mut self) {
148        for (name, default_value) in [
149            ("i32", "0"),
150            ("f64", "0"),
151            ("bool", "false"),
152            ("string", "\"\""),
153            ("color", "#000000"),
154        ] {
155            self.types.insert(
156                name.into(),
157                TypeInfo {
158                    name: name.into(),
159                    spec: name.into(),
160                    validator: None,
161                    default_value: Some(default_value.into()),
162                    metadata: new_pipeline_map(),
163                    line: 0,
164                },
165            );
166        }
167    }
168
169    /// Returns the registered inheritance default for a given type.
170    #[must_use]
171    pub fn default_value_for_type(&self, type_name: &str) -> Option<&str> {
172        self.types
173            .get(type_name)
174            .and_then(|info| info.default_value.as_deref())
175    }
176
177    /// Returns the current scope as a string path.
178    #[must_use]
179    pub fn current_scope(&self) -> String {
180        self.scope_stack.join("::")
181    }
182
183    /// Enters a new nested scope.
184    pub fn push_scope(&mut self, scope_name: impl Into<std::borrow::Cow<'a, str>>) {
185        self.scope_stack.push(scope_name.into());
186    }
187
188    /// Exits the current scope.
189    pub fn pop_scope(&mut self) {
190        if self.scope_stack.len() > 1 {
191            self.scope_stack.pop();
192        }
193    }
194
195    /// Sets a key-value pair in the map with line tracking.
196    pub fn set_value(&mut self, key: impl AsRef<str>, value: impl AsRef<str>, line: usize) {
197        let key_str = key.as_ref();
198        let key_smol: SmolStr = key_str.into();
199        self.map.insert(key_smol.clone(), value.as_ref().into());
200        self.key_line_map.insert(key_smol, line);
201    }
202
203    /// Gets a value from the map.
204    #[must_use]
205    pub fn get_value(&self, key: &str) -> Option<&str> {
206        self.map.get(key).map(AsRef::as_ref)
207    }
208
209    /// Registers a schema definition.
210    pub fn register_schema(&mut self, schema: SchemaInfo) {
211        self.schemas.insert(schema.name.clone(), schema);
212    }
213
214    /// Registers a type definition.
215    ///
216    /// # Errors
217    ///
218    /// Returns `AamlError::CircularDependency` if registering this type would
219    /// create a recursive alias cycle (e.g. `A = B`, `B = C`, `C = A`).
220    pub fn register_type(&mut self, type_def: TypeInfo) -> Result<(), AamlError> {
221        let name = type_def.name.to_string();
222        self.types.insert(type_def.name.clone(), type_def);
223
224        // Detect cycles immediately as soon as they close.
225        let mut trail = Vec::new();
226        if let Err(err @ AamlError::CircularDependency { .. }) =
227            super::Pipeline::validate_type_reference(&name, self, &mut trail)
228        {
229            return Err(err);
230        }
231
232        Ok(())
233    }
234
235    /// Registers a command (directive).
236    pub fn register_command(&mut self, command: CommandInfo<'a>) {
237        self.commands.insert(command.name.clone(), command);
238    }
239
240    /// Checks if a key has been visited (for cycle detection).
241    pub fn mark_visited(&mut self, key: &str) {
242        self.visited_keys.insert(key.into());
243    }
244
245    /// Checks if a key has been visited.
246    #[must_use]
247    pub fn is_visited(&self, key: &str) -> bool {
248        self.visited_keys.contains(key)
249    }
250
251    /// Clears visited set for a new traversal.
252    pub fn reset_visited(&mut self) {
253        self.visited_keys.clear();
254    }
255
256    /// Records an imported file.
257    pub fn record_import(&mut self, file_path: impl Into<std::borrow::Cow<'a, str>>) {
258        self.imported_files.insert(file_path.into());
259    }
260
261    /// Checks if a file has already been imported.
262    #[must_use]
263    pub fn is_imported(&self, file_path: &str) -> bool {
264        self.imported_files.contains(file_path)
265    }
266
267    /// Returns the line number where a key was defined.
268    #[must_use]
269    pub fn get_line_for_key(&self, key: &str) -> Option<usize> {
270        self.key_line_map.get(key).copied()
271    }
272}
273
274impl<'a> ExecutionDescriptor<'a> {
275    /// Creates a new execution descriptor from parsed AST.
276    pub fn new(
277        parsed_outputs: Vec<AstNode<'a>>,
278        source: impl Into<std::borrow::Cow<'a, str>>,
279    ) -> Self {
280        let line_numbers = parsed_outputs.iter().map(AstNode::line).collect();
281
282        Self {
283            line_numbers,
284            context: ExecutionContext::new(source),
285            parsed_outputs,
286            parse_tasks: Vec::new(),
287            validation_tasks: Vec::new(),
288            execution_tasks: Vec::new(),
289            stats: ExecutionStats::default(),
290        }
291    }
292
293    /// Adds a parse task to the manifest.
294    pub fn add_parse_task(&mut self, task: ParseTask<'a>) {
295        self.parse_tasks.push(task);
296    }
297
298    /// Adds multiple parse tasks.
299    pub fn add_parse_tasks(&mut self, tasks: Vec<ParseTask<'a>>) {
300        self.parse_tasks.extend(tasks);
301    }
302
303    /// Adds a validation task.
304    pub fn add_validation_task(&mut self, task: ValidationTask<'a>) {
305        self.validation_tasks.push(task);
306    }
307
308    /// Adds multiple validation tasks.
309    pub fn add_validation_tasks(&mut self, tasks: Vec<ValidationTask<'a>>) {
310        self.validation_tasks.extend(tasks);
311    }
312
313    /// Adds an execution task.
314    pub fn add_execution_task(&mut self, task: ExecutionTask<'a>) {
315        self.execution_tasks.push(task);
316    }
317
318    /// Adds multiple execution tasks.
319    pub fn add_execution_tasks(&mut self, tasks: Vec<ExecutionTask<'a>>) {
320        self.execution_tasks.extend(tasks);
321    }
322
323    /// Returns the total number of tasks.
324    #[must_use]
325    pub const fn task_count(&self) -> usize {
326        self.parse_tasks.len() + self.validation_tasks.len() + self.execution_tasks.len()
327    }
328
329    /// Returns a summary of tasks by type.
330    #[must_use]
331    pub fn task_summary(&self) -> String {
332        format!(
333            "Parse tasks: {}, Validation tasks: {}, Execution tasks: {}",
334            self.parse_tasks.len(),
335            self.validation_tasks.len(),
336            self.execution_tasks.len()
337        )
338    }
339
340    /// Updates execution statistics.
341    pub const fn update_stats(&mut self, stats: ExecutionStats) {
342        self.stats = stats;
343    }
344
345    /// Retrieves the source identifier.
346    #[must_use]
347    pub fn source(&self) -> &str {
348        &self.context.source
349    }
350
351    /// Returns a mutable reference to the execution context.
352    pub const fn context_mut(&mut self) -> &mut ExecutionContext<'a> {
353        &mut self.context
354    }
355
356    /// Returns an immutable reference to the execution context.
357    #[must_use]
358    pub const fn context(&self) -> &ExecutionContext<'a> {
359        &self.context
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    #[test]
368    fn test_execution_context_scope_management() {
369        let mut ctx = ExecutionContext::new("test.aam".to_string());
370        assert_eq!(ctx.current_scope(), "root");
371
372        ctx.push_scope("section".to_string());
373        assert_eq!(ctx.current_scope(), "root::section");
374
375        ctx.pop_scope();
376        assert_eq!(ctx.current_scope(), "root");
377    }
378
379    #[test]
380    fn test_execution_context_key_value_operations() {
381        let mut ctx = ExecutionContext::new("test.aam".to_string());
382        ctx.set_value("key1", "value1", 1);
383
384        assert_eq!(ctx.get_value("key1"), Some("value1"));
385        assert_eq!(ctx.get_line_for_key("key1"), Some(1));
386    }
387
388    #[test]
389    fn test_visited_keys_tracking() {
390        let mut ctx = ExecutionContext::new("test.aam".to_string());
391        assert!(!ctx.is_visited("key1"));
392
393        ctx.mark_visited("key1");
394        assert!(ctx.is_visited("key1"));
395
396        ctx.reset_visited();
397        assert!(!ctx.is_visited("key1"));
398    }
399
400    #[test]
401    fn test_execution_descriptor_task_management() {
402        let mut desc = ExecutionDescriptor::new(vec![], "test.aam".to_string());
403
404        desc.add_validation_task(ValidationTask::VerifySchemaExists {
405            schema_name: "MySchema".to_string().into(),
406            line: 1,
407        });
408
409        assert_eq!(desc.validation_tasks.len(), 1);
410        assert_eq!(desc.task_count(), 1);
411    }
412}