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, ®istry);
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 ®istered_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}