Skip to main content

etdl_compiler/
extension.rs

1//! Generic semantic-extension mechanism for the ETDL compiler.
2//!
3//! This is the reusable extension seam the Reliability Supplement and any
4//! future tree-event/domain supplement plug into. The core compiler only knows
5//! "a document may declare supplements; a registered extension may add
6//! validation and semantic processing." It does not embed reliability-specific
7//! logic here.
8//!
9//! The lifecycle (adapted to the existing pipeline):
10//!
11//! ```text
12//! parse
13//!   -> core validation
14//!   -> extension discovery (supplement declarations -> registry lookup)
15//!   -> extension validation
16//!   -> extension semantic processing (e.g. external probability resolution)
17//!   -> core compilation
18//!   -> code generation
19//! ```
20
21use etdl_parser::ast::EtlDocument;
22use std::collections::BTreeMap;
23
24use crate::validate::Diagnostic;
25
26/// The context an extension receives while processing a document.
27#[derive(Debug, Clone)]
28pub struct ExtensionContext<'a> {
29    pub doc: &'a EtlDocument,
30    /// Base directory for resolving relative artifact paths.
31    pub base_dir: &'a std::path::Path,
32    /// Free-form configuration for the extension (e.g. from `x-reliability`).
33    pub config: BTreeMap<String, serde_yaml::Value>,
34}
35
36impl<'a> ExtensionContext<'a> {
37    pub fn new(doc: &'a EtlDocument, base_dir: &'a std::path::Path) -> Self {
38        ExtensionContext {
39            doc,
40            base_dir,
41            config: BTreeMap::new(),
42        }
43    }
44}
45
46/// A semantic extension that plugs into the ETDL compiler.
47///
48/// Implementations SHOULD be lightweight and deterministic. An extension must
49/// not silently change core ETDL semantics; it adds validation and semantic
50/// processing only.
51pub trait EtdlExtension: Send + Sync {
52    /// The namespaced extension id, e.g. `etdl.reliability`.
53    fn id(&self) -> &str;
54
55    /// The extension version.
56    fn version(&self) -> &str;
57
58    /// Validate the document's use of this extension. Called after core
59    /// validation; diagnostics are appended to `diagnostics`.
60    fn validate(
61        &self,
62        doc: &EtlDocument,
63        context: &ExtensionContext<'_>,
64        diagnostics: &mut Vec<Diagnostic>,
65    );
66
67    /// Optional semantic processing step, run before fault-tree evaluation.
68    /// Returns diagnostics. Implementations that resolve external values (e.g.
69    /// probabilities) surface them here.
70    fn process(
71        &self,
72        _doc: &EtlDocument,
73        _context: &ExtensionContext<'_>,
74        _diagnostics: &mut Vec<Diagnostic>,
75    ) -> Box<dyn ExtensionResult + '_> {
76        Box::new(NoopExtensionResult)
77    }
78}
79
80/// Result of an extension's semantic processing step. The reliability extension
81/// returns resolved external probabilities through this; future extensions may
82/// return their own typed results.
83pub trait ExtensionResult {
84    /// The extension id that produced this result.
85    fn extension_id(&self) -> &str;
86
87    /// Basic-event probability overrides this extension's processing step
88    /// resolved, as `(override_key, value)` pairs — the same shape
89    /// `fault_tree::BasicEventOverrides` already consumes. Default: none.
90    /// An extension that resolves external values into fault-tree
91    /// probabilities (as the reliability extension does today, via its own
92    /// dedicated, hard-coded path in `Compiler::run_extensions`) overrides
93    /// this so a *generically registered* extension (`Compiler::
94    /// with_extension`) can contribute overrides the same way, without
95    /// `run_extensions` needing to know the extension's concrete result
96    /// type.
97    fn basic_event_overrides(&self) -> Vec<(String, f64)> {
98        Vec::new()
99    }
100}
101
102/// A no-op result (extensions that do no semantic processing).
103pub struct NoopExtensionResult;
104
105impl ExtensionResult for NoopExtensionResult {
106    fn extension_id(&self) -> &str {
107        ""
108    }
109}
110
111/// A deterministic registry of registered extensions.
112#[derive(Default)]
113pub struct ExtensionRegistry {
114    extensions: BTreeMap<String, Box<dyn EtdlExtension>>,
115}
116
117impl ExtensionRegistry {
118    pub fn new() -> Self {
119        ExtensionRegistry::default()
120    }
121
122    /// Register an extension. Registering a duplicate id replaces the previous
123    /// entry (deterministic last-write-wins).
124    pub fn register<E: EtdlExtension + 'static>(&mut self, extension: E) {
125        self.extensions
126            .insert(extension.id().to_string(), Box::new(extension));
127    }
128
129    pub fn lookup(&self, id: &str) -> Option<&dyn EtdlExtension> {
130        self.extensions.get(id).map(|b| b.as_ref())
131    }
132
133    pub fn contains(&self, id: &str) -> bool {
134        self.extensions.contains_key(id)
135    }
136
137    /// Registered extension ids, sorted (deterministic).
138    pub fn list(&self) -> Vec<&str> {
139        self.extensions.keys().map(|s| s.as_str()).collect()
140    }
141}
142
143/// Built-in extensions shipped with the compiler.
144pub fn builtin_registry() -> ExtensionRegistry {
145    let mut registry = ExtensionRegistry::new();
146    // Domain-neutral, always compiled in — not gated behind the
147    // `reliability` feature.
148    registry.register(crate::tree_event::TreeEventExtension::new());
149    #[cfg(feature = "reliability")]
150    {
151        registry.register(crate::reliability::ReliabilityExtension::new());
152    }
153    registry
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    struct TestExt;
161
162    impl EtdlExtension for TestExt {
163        fn id(&self) -> &str {
164            "etdl.test"
165        }
166        fn version(&self) -> &str {
167            "1.0"
168        }
169        fn validate(
170            &self,
171            _doc: &EtlDocument,
172            _context: &ExtensionContext<'_>,
173            _diagnostics: &mut Vec<Diagnostic>,
174        ) {
175        }
176    }
177
178    #[test]
179    fn registry_is_deterministic() {
180        let mut r = ExtensionRegistry::new();
181        r.register(TestExt);
182        r.register(TestExt);
183        assert!(r.contains("etdl.test"));
184        assert!(r.lookup("etdl.test").is_some());
185        assert_eq!(r.list(), vec!["etdl.test"]);
186    }
187
188    #[test]
189    fn lookup_missing_is_none() {
190        let r = ExtensionRegistry::new();
191        assert!(r.lookup("etdl.nope").is_none());
192    }
193}