Skip to main content

etdl_compiler/
lib.rs

1//! Compiler pipeline for the Event Tree Definition Language (ETDL).
2//!
3//! Validates `.etdl` documents, resolves fault tree top-event probabilities
4//! ([IEC 61025:2006](https://github.com/usamassem/etdl-specification)), and
5//! generates service-local code. Reliability — event trees (IEC 62502),
6//! fault trees (IEC 61025), retry policies, SLAs — becomes a build-time,
7//! machine-checked artifact instead of scattered runtime guesses.
8//!
9//! # Pipeline
10//!
11//! 1. [`validate::validate_document`] — structural and semantic diagnostics (E-1xx,
12//!    V-1xx..V-5xx, W-4xx)
13//! 2. [`fault_tree::resolve_fault_trees`] — exact top-event probability evaluation
14//!    (AND/OR/NOT/XOR/VOTING gates, exponential failure model)
15//! 3. [`typeck`] — ECEL condition type-checking against AsyncAPI schemas
16//! 4. [`codegen::CodeGenerator`] — backend trait; [`codegen::RustCodeGenerator`]
17//!    emits async handlers with embedded probabilities, retry policies, and
18//!    `etdl-core` instrumentation
19//!
20//! # Example
21//!
22//! ```no_run
23//! use etdl_compiler::Compiler;
24//! use etdl_parser::{parse_document_from_file, load_asyncapi_imports};
25//! use std::path::Path;
26//!
27//! let base = Path::new(".");
28//! let doc = parse_document_from_file(&base.join("order-fulfillment.etdl"))?;
29//! let registry = load_asyncapi_imports(&doc, base)?;
30//! let result = Compiler::new().compile(&doc, &registry);
31//! assert!(result.diagnostics.iter().all(|d| !d.is_error()));
32//! assert!(result.rust_output.is_some());
33//! # Ok::<(), String>(())
34//! ```
35
36use etdl_parser::ast::EtlDocument;
37use etdl_parser::asyncapi::AsyncApiRegistry;
38
39pub mod codegen;
40pub mod extension;
41pub mod fault_tree;
42#[cfg(feature = "reliability")]
43pub mod reliability;
44pub mod stdlib;
45pub mod tree_event;
46mod typeck;
47pub mod validate;
48
49pub use codegen::{CodeGenerator, RustCodeGenerator};
50pub use extension::{EtdlExtension, ExtensionContext, ExtensionRegistry};
51pub use validate::Diagnostic;
52
53pub struct Compiler {
54    pub rust_codegen: RustCodeGenerator,
55    /// Resolves declared `libraries:` (standard/domain/optional/user). The
56    /// built-in standard library and base_dir-relative user libraries
57    /// resolve automatically; add optional-library search paths with
58    /// [`Compiler::with_library_search_path`].
59    pub library_resolver: stdlib::LibraryResolver,
60    /// Extensions registered in addition to the built-in ones (the
61    /// Reliability and Tree Event supplements, handled internally by
62    /// `run_extensions` exactly as before — unaffected by this field).
63    /// This is the entry point a non-core supplement (core spec Section
64    /// 11.4/11.5 — e.g. a third-party `etdl.chain` implementation)
65    /// registers itself through via [`Compiler::with_extension`], so its
66    /// `validate`/`process` (core spec Section 11.3) actually run during
67    /// [`Compiler::compile`]/[`Compiler::validate`], not just something a
68    /// third party could theoretically implement.
69    extensions: Vec<Box<dyn EtdlExtension>>,
70}
71
72/// A complete compilation result including optional reliability provenance.
73#[derive(Debug, Clone)]
74pub struct CompilationResult {
75    pub diagnostics: Vec<Diagnostic>,
76    pub rust_output: Option<String>,
77    /// Reliability build manifest, present when the document declares the
78    /// reliability supplement and external sources were resolved.
79    pub build_manifest: Option<serde_json::Value>,
80    /// Identity of every library actually resolved for this build (name,
81    /// version, built-in/optional/user), independent of the `reliability`
82    /// feature — a document using `libraries:` gets this without needing
83    /// any optional feature enabled.
84    pub resolved_libraries: Vec<stdlib::LibraryProvenance>,
85}
86
87impl Compiler {
88    pub fn new() -> Self {
89        Compiler {
90            rust_codegen: RustCodeGenerator::new(),
91            library_resolver: stdlib::LibraryResolver::new(),
92            extensions: Vec::new(),
93        }
94    }
95
96    /// Add a search directory for optional (non-`std.*`) libraries. Checked
97    /// in the order added; never consulted for names under the reserved
98    /// `std.` namespace.
99    pub fn with_library_search_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
100        self.library_resolver = self.library_resolver.with_search_path(path);
101        self
102    }
103
104    /// Register an additional supplement extension (core spec Section 11.5:
105    /// "Defining a New Supplement"). Its `validate`/`process` (core spec
106    /// Section 11.3) run during [`Compiler::validate`]/[`Compiler::compile`]
107    /// exactly like the built-in Reliability/Tree Event extensions do,
108    /// gated the same way: only for a document that actually declares the
109    /// extension's `id()` under `supplements:` (core spec Section 5.1.1).
110    /// This does not replace or reorder the built-in extensions, which
111    /// [`Compiler::run_extensions`] continues to handle exactly as before —
112    /// it adds a place for anything else to plug in.
113    pub fn with_extension(mut self, extension: Box<dyn EtdlExtension>) -> Self {
114        self.extensions.push(extension);
115        self
116    }
117
118    /// Run the full validation pipeline (semantic checks, fault-tree
119    /// resolution, probability validation, ECEL type checking) without
120    /// generating any code.
121    pub fn validate(
122        &self,
123        doc: &EtlDocument,
124        asyncapi_registry: &AsyncApiRegistry,
125    ) -> Vec<Diagnostic> {
126        self.validate_with_base(doc, asyncapi_registry, std::path::Path::new("."))
127    }
128
129    /// Validate, resolving external reliability sources relative to `base_dir`.
130    pub fn validate_with_base(
131        &self,
132        doc: &EtlDocument,
133        asyncapi_registry: &AsyncApiRegistry,
134        base_dir: &std::path::Path,
135    ) -> Vec<Diagnostic> {
136        let mut diagnostics = Vec::new();
137
138        // Resolve `libraries:` and splice referenced definitions into the
139        // fault trees that use them BEFORE structural validation runs, so a
140        // qualified library reference (e.g. `std.events.NetworkTimeout`)
141        // validates exactly like any other basic-event id. The original
142        // `doc` is never mutated.
143        let (expanded_doc, resolved_libs, lib_errors) =
144            stdlib::expand_libraries(doc, base_dir, &self.library_resolver);
145        validate::validate_libraries(doc, &lib_errors, &mut diagnostics);
146        let doc = &expanded_doc;
147        let _ = &resolved_libs;
148
149        let registered_extension_ids: Vec<&str> =
150            self.extensions.iter().map(|e| e.id()).collect();
151        validate::validate_document_with_extensions(
152            doc,
153            asyncapi_registry,
154            &registered_extension_ids,
155            &mut diagnostics,
156        );
157
158        // The Generic Tree Event Supplement is structural-only (no fault-tree
159        // overrides to feed forward), so it is validated directly here
160        // rather than through `run_extensions`'s override-collecting path —
161        // purely additive; nothing about the reliability extension's own
162        // call below changed.
163        let (_trees, tree_diagnostics) = tree_event::parse_and_validate_trees(doc);
164        diagnostics.extend(tree_diagnostics);
165
166        if diagnostics.iter().any(|d| d.is_error()) {
167            return diagnostics;
168        }
169
170        // Run registered extensions' semantic processing (e.g. external
171        // probability resolution) so that values they supply feed evaluation.
172        let (overrides, _manifest) = self.run_extensions(doc, base_dir, &mut diagnostics);
173
174        let fault_tree_probs =
175            fault_tree::resolve_fault_trees_with_overrides(doc, &overrides, &mut diagnostics);
176        let resolved_probabilities =
177            validate::resolve_probability_links(doc, &fault_tree_probs, &mut diagnostics);
178
179        validate::validate_probability_sums(doc, &resolved_probabilities, &mut diagnostics);
180
181        if diagnostics.iter().any(|d| d.is_error()) {
182            return diagnostics;
183        }
184
185        typeck::type_check_conditions(doc, asyncapi_registry, &mut diagnostics);
186
187        diagnostics
188    }
189
190    pub fn compile(
191        &self,
192        doc: &EtlDocument,
193        asyncapi_registry: &AsyncApiRegistry,
194    ) -> CompilationResult {
195        self.compile_with_base(doc, asyncapi_registry, std::path::Path::new("."))
196    }
197
198    /// Compile with a base directory for resolving relative reliability
199    /// artifact paths.
200    pub fn compile_with_base(
201        &self,
202        doc: &EtlDocument,
203        asyncapi_registry: &AsyncApiRegistry,
204        base_dir: &std::path::Path,
205    ) -> CompilationResult {
206        // Resolve `libraries:` once here; `validate_with_base` also expands
207        // (idempotently, from the already-expanded document) since it is a
208        // public entry point in its own right and must not require callers
209        // to pre-expand.
210        let (expanded_doc, resolved_libs, _lib_errors) =
211            stdlib::expand_libraries(doc, base_dir, &self.library_resolver);
212        let doc = &expanded_doc;
213        let resolved_libraries: Vec<stdlib::LibraryProvenance> =
214            resolved_libs.iter().map(|l| l.provenance()).collect();
215
216        let mut diagnostics = self.validate_with_base(doc, asyncapi_registry, base_dir);
217
218        let has_errors = diagnostics.iter().any(|d| d.is_error());
219        if has_errors {
220            return CompilationResult {
221                diagnostics,
222                rust_output: None,
223                build_manifest: None,
224                resolved_libraries,
225            };
226        }
227
228        // Extensions: resolve external probability sources to deterministic
229        // scalars BEFORE fault-tree evaluation. Preserves the build-time
230        // resolution model; nothing is resolved at runtime.
231        let (overrides, manifest) = self.run_extensions(doc, base_dir, &mut diagnostics);
232
233        let build_manifest = manifest.as_ref().and_then(|m| serde_json::to_value(m).ok());
234
235        let fault_tree_probs =
236            fault_tree::resolve_fault_trees_with_overrides(doc, &overrides, &mut diagnostics);
237
238        let mut rust_output = String::new();
239        let gen_result = self.rust_codegen.generate_all(
240            doc,
241            &fault_tree_probs,
242            asyncapi_registry,
243            &mut diagnostics,
244        );
245
246        if let Ok(gen_code) = gen_result {
247            rust_output = gen_code;
248        }
249
250        CompilationResult {
251            diagnostics,
252            rust_output: if rust_output.is_empty() {
253                None
254            } else {
255                Some(rust_output)
256            },
257            build_manifest,
258            resolved_libraries,
259        }
260    }
261
262    /// Run registered extensions' semantic processing.
263    ///
264    /// Each extension's `process` step may resolve external values (e.g.
265    /// probabilities). The returned map is the aggregated basic-event
266    /// probability overrides feeding the existing fault-tree evaluator. With
267    /// the `reliability` feature disabled, this returns an empty override map.
268    fn run_extensions(
269        &self,
270        doc: &EtlDocument,
271        base_dir: &std::path::Path,
272        diagnostics: &mut Vec<Diagnostic>,
273    ) -> (fault_tree::BasicEventOverrides, Option<serde_json::Value>) {
274        // Always `mut`: with the `reliability` feature disabled there is no
275        // built-in resolver to populate it below, but a generically
276        // registered extension (`Compiler::with_extension`) still can,
277        // regardless of that feature.
278        let mut overrides = fault_tree::BasicEventOverrides::new();
279        #[cfg(feature = "reliability")]
280        let manifest: Option<serde_json::Value> = {
281            let (resolved_events, m) = reliability::resolve_reliability(doc, base_dir, diagnostics);
282            overrides.extend(
283                resolved_events
284                    .iter()
285                    .map(|r| (r.override_key(), r.resolved.value)),
286            );
287            m.as_ref().and_then(|m| serde_json::to_value(m).ok())
288        };
289        // The built-in reliability resolver is compiled out without the
290        // `reliability` feature; `doc`/`base_dir`/`diagnostics` remain real
291        // parameters regardless, used below by any generically registered
292        // extension (`Compiler::with_extension`).
293        #[cfg(not(feature = "reliability"))]
294        let manifest: Option<serde_json::Value> = None;
295
296        // Additionally registered extensions (`Compiler::with_extension`):
297        // run validate() then process() for each one the document actually
298        // declares under `supplements:` (the same declare-to-opt-in gate
299        // the built-in extensions already use), merging any basic-event
300        // overrides they resolve. Their manifests, if any, are not folded
301        // into the single `manifest` value above — a caller wanting a
302        // registered extension's own output reads it from that extension's
303        // `ExtensionResult` directly in a caller-side integration, since
304        // this method's `Option<serde_json::Value>` return shape predates
305        // there being more than one extension.
306        for extension in &self.extensions {
307            if !crate::validate::declares_supplement(doc, extension.id()) {
308                continue;
309            }
310            let context = crate::extension::ExtensionContext::new(doc, base_dir);
311            extension.validate(doc, &context, diagnostics);
312            if diagnostics.iter().any(|d| d.is_error()) {
313                continue;
314            }
315            let result = extension.process(doc, &context, diagnostics);
316            overrides.extend(result.basic_event_overrides());
317        }
318
319        (overrides, manifest)
320    }
321}
322
323impl Default for Compiler {
324    fn default() -> Self {
325        Self::new()
326    }
327}