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}