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/ETDL-lang/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 diagnostics;
41pub mod extension;
42pub mod fault_tree;
43pub mod performance;
44#[cfg(feature = "reliability")]
45pub mod reliability;
46pub mod safety;
47pub mod security;
48pub mod stdlib;
49pub mod tree_event;
50mod typeck;
51pub mod validate;
52#[cfg(feature = "plugins")]
53pub mod wasm_extension;
54
55pub use codegen::{CodeGenerator, GeneratedFile, RustCodeGenerator};
56pub use extension::{EtdlExtension, ExtensionContext, ExtensionRegistry, SupplementDescriptor};
57pub use validate::Diagnostic;
58
59pub struct Compiler {
60 pub rust_codegen: RustCodeGenerator,
61 /// Resolves declared `libraries:` (standard/domain/optional/user). The
62 /// built-in standard library and base_dir-relative user libraries
63 /// resolve automatically; add optional-library search paths with
64 /// [`Compiler::with_library_search_path`].
65 pub library_resolver: stdlib::LibraryResolver,
66 /// Extensions run through the generic, registry-driven
67 /// `EtdlExtension::validate`/`process` path (`Compiler::run_extensions`).
68 /// The Reliability and Tree Event supplements are *not* here — each has
69 /// its own bespoke, special-cased call elsewhere in this pipeline
70 /// (`run_extensions`'s hard-coded `resolve_reliability` call,
71 /// `validate_with_base`'s direct `tree_event::parse_and_validate_trees`
72 /// call). Every other supplement, built-in or third-party, goes through
73 /// this field instead of getting its own bespoke pipeline code:
74 /// [`Compiler::new`] seeds it with the Performance Supplement (always
75 /// available, like Tree Event, but wired generically rather than
76 /// special-cased — see `performance` module docs), and
77 /// [`Compiler::with_extension`] is the same seam a third-party
78 /// supplement (core spec Section 11.4/11.5 — e.g. a future
79 /// `etdl.chain`) uses to register itself, so its `validate`/`process`
80 /// (core spec Section 11.3) actually run during
81 /// [`Compiler::compile`]/[`Compiler::validate`].
82 extensions: Vec<Box<dyn EtdlExtension>>,
83}
84
85/// A complete compilation result including optional reliability provenance.
86#[derive(Debug, Clone)]
87pub struct CompilationResult {
88 pub diagnostics: Vec<Diagnostic>,
89 pub rust_output: Option<String>,
90 /// Reliability build manifest, present when the document declares the
91 /// reliability supplement and external sources were resolved.
92 pub build_manifest: Option<serde_json::Value>,
93 /// Identity of every library actually resolved for this build (name,
94 /// version, built-in/optional/user), independent of the `reliability`
95 /// feature — a document using `libraries:` gets this without needing
96 /// any optional feature enabled.
97 pub resolved_libraries: Vec<stdlib::LibraryProvenance>,
98}
99
100/// Result of compiling for one arbitrary target generator
101/// ([`Compiler::compile_target`]/[`Compiler::compile_target_with_base`]) —
102/// the target-neutral counterpart to [`CompilationResult`], which stays
103/// Rust-specific (`rust_output: Option<String>`) for backward compatibility.
104#[derive(Debug, Clone)]
105pub struct TargetCompilationResult {
106 pub diagnostics: Vec<Diagnostic>,
107 /// The generated files, present iff generation succeeded with no
108 /// upstream errors. Empty output (a generator returning zero files) is
109 /// treated the same as `None`, matching `CompilationResult::rust_output`.
110 pub files: Option<Vec<codegen::GeneratedFile>>,
111 pub build_manifest: Option<serde_json::Value>,
112 pub resolved_libraries: Vec<stdlib::LibraryProvenance>,
113}
114
115/// The target-neutral part of the pipeline (library expansion, validation,
116/// extension processing, fault-tree probability resolution) — computed once
117/// and shared by every target's compilation, so no target implementation
118/// (Rust or otherwise) re-runs parsing, semantic validation, or fault-tree
119/// evaluation. See `docs/architecture/targets.md`.
120struct Prepared {
121 expanded_doc: EtlDocument,
122 fault_tree_probs: std::collections::BTreeMap<String, f64>,
123 diagnostics: Vec<Diagnostic>,
124 build_manifest: Option<serde_json::Value>,
125 resolved_libraries: Vec<stdlib::LibraryProvenance>,
126}
127
128impl Compiler {
129 pub fn new() -> Self {
130 Compiler {
131 rust_codegen: RustCodeGenerator::new(),
132 library_resolver: stdlib::LibraryResolver::new(),
133 // The Performance and Safety Supplements have no bespoke
134 // pipeline code of their own (see their module docs) — seeding
135 // them here is the entire "built-in" wiring either gets;
136 // `run_extensions`'s existing generic loop over `extensions`
137 // does the rest, the same as it would for a third-party
138 // `with_extension` caller.
139 extensions: vec![
140 Box::new(performance::PerformanceExtension::new()),
141 Box::new(safety::SafetyExtension::new()),
142 Box::new(diagnostics::DiagnosticsExtension::new()),
143 Box::new(security::SecurityExtension::new()),
144 ],
145 }
146 }
147
148 /// Add a search directory for optional (non-`std.*`) libraries. Checked
149 /// in the order added; never consulted for names under the reserved
150 /// `std.` namespace.
151 pub fn with_library_search_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
152 self.library_resolver = self.library_resolver.with_search_path(path);
153 self
154 }
155
156 /// Register an additional supplement extension (core spec Section 11.5:
157 /// "Defining a New Supplement"). Its `validate`/`process` (core spec
158 /// Section 11.3) run during [`Compiler::validate`]/[`Compiler::compile`]
159 /// exactly like the built-in Reliability/Tree Event extensions do,
160 /// gated the same way: only for a document that actually declares the
161 /// extension's `id()` under `supplements:` (core spec Section 5.1.1).
162 /// This does not replace or reorder the built-in extensions, which
163 /// [`Compiler::run_extensions`] continues to handle exactly as before —
164 /// it adds a place for anything else to plug in.
165 pub fn with_extension(mut self, extension: Box<dyn EtdlExtension>) -> Self {
166 self.extensions.push(extension);
167 self
168 }
169
170 /// Run the full validation pipeline (semantic checks, fault-tree
171 /// resolution, probability validation, ECEL type checking) without
172 /// generating any code.
173 pub fn validate(
174 &self,
175 doc: &EtlDocument,
176 asyncapi_registry: &AsyncApiRegistry,
177 ) -> Vec<Diagnostic> {
178 self.validate_with_base(doc, asyncapi_registry, std::path::Path::new("."))
179 }
180
181 /// Validate, resolving external reliability sources relative to `base_dir`.
182 pub fn validate_with_base(
183 &self,
184 doc: &EtlDocument,
185 asyncapi_registry: &AsyncApiRegistry,
186 base_dir: &std::path::Path,
187 ) -> Vec<Diagnostic> {
188 let mut diagnostics = Vec::new();
189
190 // Resolve `libraries:` and splice referenced definitions into the
191 // fault trees that use them BEFORE structural validation runs, so a
192 // qualified library reference (e.g. `std.events.NetworkTimeout`)
193 // validates exactly like any other basic-event id. The original
194 // `doc` is never mutated.
195 let (expanded_doc, resolved_libs, lib_errors) =
196 stdlib::expand_libraries(doc, base_dir, &self.library_resolver);
197 validate::validate_libraries(doc, &lib_errors, &mut diagnostics);
198 let doc = &expanded_doc;
199 let _ = &resolved_libs;
200
201 let registered_extension_ids: Vec<&str> =
202 self.extensions.iter().map(|e| e.id()).collect();
203 validate::validate_document_with_extensions(
204 doc,
205 asyncapi_registry,
206 ®istered_extension_ids,
207 &mut diagnostics,
208 );
209
210 // The Generic Tree Event Supplement is structural-only (no fault-tree
211 // overrides to feed forward), so it is validated directly here
212 // rather than through `run_extensions`'s override-collecting path —
213 // purely additive; nothing about the reliability extension's own
214 // call below changed.
215 let (_trees, tree_diagnostics) = tree_event::parse_and_validate_trees(doc);
216 diagnostics.extend(tree_diagnostics);
217
218 if diagnostics.iter().any(|d| d.is_error()) {
219 return diagnostics;
220 }
221
222 // Run registered extensions' semantic processing (e.g. external
223 // probability resolution) so that values they supply feed evaluation.
224 let (overrides, _manifest) = self.run_extensions(doc, base_dir, &mut diagnostics);
225
226 let fault_tree_probs =
227 fault_tree::resolve_fault_trees_with_overrides(doc, &overrides, &mut diagnostics);
228 let resolved_probabilities =
229 validate::resolve_probability_links(doc, &fault_tree_probs, &mut diagnostics);
230
231 validate::validate_probability_sums(doc, &resolved_probabilities, &mut diagnostics);
232
233 if diagnostics.iter().any(|d| d.is_error()) {
234 return diagnostics;
235 }
236
237 typeck::type_check_conditions(doc, asyncapi_registry, &mut diagnostics);
238
239 diagnostics
240 }
241
242 pub fn compile(
243 &self,
244 doc: &EtlDocument,
245 asyncapi_registry: &AsyncApiRegistry,
246 ) -> CompilationResult {
247 self.compile_with_base(doc, asyncapi_registry, std::path::Path::new("."))
248 }
249
250 /// Compile with a base directory for resolving relative reliability
251 /// artifact paths.
252 pub fn compile_with_base(
253 &self,
254 doc: &EtlDocument,
255 asyncapi_registry: &AsyncApiRegistry,
256 base_dir: &std::path::Path,
257 ) -> CompilationResult {
258 let prepared = self.prepare(doc, asyncapi_registry, base_dir);
259 if prepared.diagnostics.iter().any(|d| d.is_error()) {
260 return CompilationResult {
261 diagnostics: prepared.diagnostics,
262 rust_output: None,
263 build_manifest: prepared.build_manifest,
264 resolved_libraries: prepared.resolved_libraries,
265 };
266 }
267
268 let mut diagnostics = prepared.diagnostics;
269 // "generated" is a placeholder stem: compile_with_base predates
270 // per-file naming (callers only ever read `rust_output`'s string
271 // content, never a filename derived from it), so nothing observes
272 // this value — it exists only to satisfy generate_all's signature,
273 // which every target (not just this one) now takes a stem through.
274 let gen_result = self.rust_codegen.generate_all(
275 &prepared.expanded_doc,
276 &prepared.fault_tree_probs,
277 asyncapi_registry,
278 "generated",
279 &mut diagnostics,
280 );
281 let rust_output = gen_result
282 .ok()
283 .and_then(|files| files.into_iter().next())
284 .map(|f| f.contents)
285 .filter(|s| !s.is_empty());
286
287 CompilationResult {
288 diagnostics,
289 rust_output,
290 build_manifest: prepared.build_manifest,
291 resolved_libraries: prepared.resolved_libraries,
292 }
293 }
294
295 /// Compile for an arbitrary registered target (see `etdl-cli`'s target
296 /// registry) rather than the built-in Rust target specifically. `stem`
297 /// is the input document's filename without extension, used for output
298 /// naming exactly like `compile_with_base`'s Rust path already does.
299 pub fn compile_target(
300 &self,
301 doc: &EtlDocument,
302 asyncapi_registry: &AsyncApiRegistry,
303 generator: &dyn CodeGenerator,
304 stem: &str,
305 ) -> TargetCompilationResult {
306 self.compile_target_with_base(doc, asyncapi_registry, std::path::Path::new("."), generator, stem)
307 }
308
309 /// [`Compiler::compile_target`] with a base directory for resolving
310 /// relative reliability artifact paths.
311 pub fn compile_target_with_base(
312 &self,
313 doc: &EtlDocument,
314 asyncapi_registry: &AsyncApiRegistry,
315 base_dir: &std::path::Path,
316 generator: &dyn CodeGenerator,
317 stem: &str,
318 ) -> TargetCompilationResult {
319 let prepared = self.prepare(doc, asyncapi_registry, base_dir);
320 if prepared.diagnostics.iter().any(|d| d.is_error()) {
321 return TargetCompilationResult {
322 diagnostics: prepared.diagnostics,
323 files: None,
324 build_manifest: prepared.build_manifest,
325 resolved_libraries: prepared.resolved_libraries,
326 };
327 }
328
329 let mut diagnostics = prepared.diagnostics;
330 let gen_result = generator.generate_all(
331 &prepared.expanded_doc,
332 &prepared.fault_tree_probs,
333 asyncapi_registry,
334 stem,
335 &mut diagnostics,
336 );
337 let files = gen_result.ok().filter(|files| !files.is_empty());
338
339 TargetCompilationResult {
340 diagnostics,
341 files,
342 build_manifest: prepared.build_manifest,
343 resolved_libraries: prepared.resolved_libraries,
344 }
345 }
346
347 /// Shared pipeline for every target: library expansion, validation,
348 /// extension processing, fault-tree probability resolution. Faithfully
349 /// mirrors what `compile_with_base` always did inline (including
350 /// `validate_with_base`'s own idempotent re-expansion of `libraries:` —
351 /// see its doc comment) — extracted here, unchanged, so a second target
352 /// can share it instead of duplicating it.
353 fn prepare(
354 &self,
355 doc: &EtlDocument,
356 asyncapi_registry: &AsyncApiRegistry,
357 base_dir: &std::path::Path,
358 ) -> Prepared {
359 let (expanded_doc, resolved_libs, _lib_errors) =
360 stdlib::expand_libraries(doc, base_dir, &self.library_resolver);
361 let resolved_libraries: Vec<stdlib::LibraryProvenance> =
362 resolved_libs.iter().map(|l| l.provenance()).collect();
363
364 let mut diagnostics = self.validate_with_base(&expanded_doc, asyncapi_registry, base_dir);
365 let has_errors = diagnostics.iter().any(|d| d.is_error());
366 if has_errors {
367 return Prepared {
368 expanded_doc,
369 fault_tree_probs: std::collections::BTreeMap::new(),
370 diagnostics,
371 build_manifest: None,
372 resolved_libraries,
373 };
374 }
375
376 // Extensions: resolve external probability sources to deterministic
377 // scalars BEFORE fault-tree evaluation. Preserves the build-time
378 // resolution model; nothing is resolved at runtime.
379 let (overrides, manifest) = self.run_extensions(&expanded_doc, base_dir, &mut diagnostics);
380 let build_manifest = manifest.as_ref().and_then(|m| serde_json::to_value(m).ok());
381 let fault_tree_probs = fault_tree::resolve_fault_trees_with_overrides(
382 &expanded_doc,
383 &overrides,
384 &mut diagnostics,
385 );
386
387 Prepared {
388 expanded_doc,
389 fault_tree_probs,
390 diagnostics,
391 build_manifest,
392 resolved_libraries,
393 }
394 }
395
396 /// Run registered extensions' semantic processing.
397 ///
398 /// Each extension's `process` step may resolve external values (e.g.
399 /// probabilities). The returned map is the aggregated basic-event
400 /// probability overrides feeding the existing fault-tree evaluator. With
401 /// the `reliability` feature disabled, this returns an empty override map.
402 fn run_extensions(
403 &self,
404 doc: &EtlDocument,
405 base_dir: &std::path::Path,
406 diagnostics: &mut Vec<Diagnostic>,
407 ) -> (fault_tree::BasicEventOverrides, Option<serde_json::Value>) {
408 // Always `mut`: with the `reliability` feature disabled there is no
409 // built-in resolver to populate it below, but a generically
410 // registered extension (`Compiler::with_extension`) still can,
411 // regardless of that feature.
412 let mut overrides = fault_tree::BasicEventOverrides::new();
413 #[cfg(feature = "reliability")]
414 let manifest: Option<serde_json::Value> = {
415 let (resolved_events, m) = reliability::resolve_reliability(doc, base_dir, diagnostics);
416 overrides.extend(
417 resolved_events
418 .iter()
419 .map(|r| (r.override_key(), r.resolved.value)),
420 );
421 m.as_ref().and_then(|m| serde_json::to_value(m).ok())
422 };
423 // The built-in reliability resolver is compiled out without the
424 // `reliability` feature; `doc`/`base_dir`/`diagnostics` remain real
425 // parameters regardless, used below by any generically registered
426 // extension (`Compiler::with_extension`).
427 #[cfg(not(feature = "reliability"))]
428 let manifest: Option<serde_json::Value> = None;
429
430 // Additionally registered extensions (`Compiler::with_extension`):
431 // run validate() then process() for each one the document actually
432 // declares under `supplements:` (the same declare-to-opt-in gate
433 // the built-in extensions already use), merging any basic-event
434 // overrides they resolve. Their manifests, if any, are not folded
435 // into the single `manifest` value above — a caller wanting a
436 // registered extension's own output reads it from that extension's
437 // `ExtensionResult` directly in a caller-side integration, since
438 // this method's `Option<serde_json::Value>` return shape predates
439 // there being more than one extension.
440 for extension in &self.extensions {
441 if !crate::validate::declares_supplement(doc, extension.id()) {
442 continue;
443 }
444 let context = crate::extension::ExtensionContext::new(doc, base_dir);
445 extension.validate(doc, &context, diagnostics);
446 if diagnostics.iter().any(|d| d.is_error()) {
447 continue;
448 }
449 let result = extension.process(doc, &context, diagnostics);
450 overrides.extend(result.basic_event_overrides());
451 }
452
453 (overrides, manifest)
454 }
455}
456
457impl Default for Compiler {
458 fn default() -> Self {
459 Self::new()
460 }
461}