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/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, &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 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            &registered_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}