tlq_fhirpath/
engine.rs

1//! Main FHIRPath engine
2//!
3//! Orchestrates the compilation pipeline: Parse → AST → HIR → VM Plan → Execution
4
5use crate::analyzer::{self, Analyzer};
6use crate::codegen::CodeGenerator;
7use crate::context::Context;
8use crate::error::{Error, Result};
9use crate::functions::FunctionRegistry;
10use crate::resolver::ResourceResolver;
11use crate::types::TypeRegistry;
12use crate::value::{Collection, Value};
13use crate::variables::VariableRegistry;
14use crate::vm::Plan;
15use lru::LruCache;
16use std::sync::{Arc, Mutex};
17use tlq_fhir_context::{DefaultFhirContext, FhirContext};
18
19#[derive(Clone, Debug, Default)]
20pub struct CompileOptions {
21    /// Optional base type name used for semantic type annotation and (when `strict`)
22    /// StructureDefinition-based path validation.
23    pub base_type: Option<String>,
24    /// If `true`, invalid path navigation on resolvable FHIR types errors at compile time.
25    pub strict: bool,
26}
27
28#[derive(Clone, Debug)]
29pub struct EvalOptions {
30    /// Optional base type name used for compile-time typing/validation.
31    pub base_type: Option<String>,
32    /// If `true`, enables compile-time strict validation (independent of `base_type` presence).
33    pub strict: bool,
34    /// If `true` and `base_type` is not provided, attempt to infer a base type from the
35    /// runtime resource (`resourceType`) for relative paths (e.g., `name.given`).
36    pub infer_base_type: bool,
37}
38
39impl Default for EvalOptions {
40    fn default() -> Self {
41        Self {
42            base_type: None,
43            strict: false,
44            infer_base_type: true,
45        }
46    }
47}
48
49/// Main FHIRPath engine
50///
51/// Requires a FHIR context for runtime type resolution from StructureDefinitions.
52/// The context is used during AST → HIR compilation to infer types for path navigation.
53///
54/// Optionally accepts a custom ResourceResolver for overriding the `resolve()` function
55/// behavior. This is useful for database-backed resolution or other custom logic.
56pub struct Engine {
57    type_registry: Arc<TypeRegistry>,
58    function_registry: Arc<FunctionRegistry>,
59    cache: Arc<Mutex<LruCache<String, Arc<Plan>>>>,
60    variable_registry: Arc<Mutex<VariableRegistry>>,
61    fhir_context: Arc<dyn FhirContext>,
62    resource_resolver: Option<Arc<dyn ResourceResolver>>,
63}
64
65impl Engine {
66    /// Create a new engine with FHIR context and custom ResourceResolver
67    ///
68    /// The FHIR context provides StructureDefinition lookup for type inference during
69    /// HIR generation. This allows the engine to work with any Implementation Guide
70    /// without requiring static compilation of all FHIR types.
71    ///
72    /// The custom resolver will be used by the `resolve()` function to resolve
73    /// FHIR references. This is useful for database-backed resolution or other
74    /// custom logic.
75    ///
76    /// # Example
77    ///
78    /// ```rust,ignore
79    /// use fhirpath_engine::{Engine, ResourceResolver};
80    /// use std::sync::Arc;
81    ///
82    /// struct MyResolver { /* ... */ }
83    /// impl ResourceResolver for MyResolver { /* ... */ }
84    ///
85    /// let resolver = Arc::new(MyResolver::new());
86    /// let engine = Engine::new_with_resolver(fhir_context, Some(resolver));
87    /// ```
88    pub fn new(context: Arc<dyn FhirContext>, resolver: Option<Arc<dyn ResourceResolver>>) -> Self {
89        Self {
90            type_registry: Arc::new(TypeRegistry::new()),
91            function_registry: Arc::new(FunctionRegistry::new()),
92            cache: Arc::new(Mutex::new(LruCache::new(
93                std::num::NonZeroUsize::new(1000).unwrap(),
94            ))),
95            variable_registry: Arc::new(Mutex::new(VariableRegistry::new())),
96            fhir_context: context,
97            resource_resolver: resolver,
98        }
99    }
100
101    /// Create an engine with a default FHIR context loaded from registry cache (async).
102    ///
103    /// The engine will attempt to load the base FHIR package for the specified version
104    /// from the registry cache (~/.fhir/packages/). If the package is not found in cache,
105    /// it will download from Simplifier.
106    ///
107    /// Supported versions: "R4", "R4B", "R5"
108    ///
109    /// # Example
110    ///
111    /// ```rust,no_run
112    /// use tlq_fhirpath::Engine;
113    ///
114    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
115    /// // Load R5 context from cache or download
116    /// let engine = Engine::with_fhir_version("R5").await?;
117    /// # Ok(())
118    /// # }
119    /// ```
120    pub async fn with_fhir_version(version: &str) -> Result<Self> {
121        let context: Arc<dyn FhirContext> = Arc::new(
122            DefaultFhirContext::from_fhir_version_async(None, version)
123                .await
124                .map_err(|e| {
125                    Error::EvaluationError(format!(
126                        "Failed to load FHIR package {}: {}",
127                        version, e
128                    ))
129                })?,
130        );
131
132        Ok(Self::new(context, None))
133    }
134
135    pub fn fhir_context(&self) -> &Arc<dyn FhirContext> {
136        &self.fhir_context
137    }
138
139    /// Get the custom resource resolver (if any)
140    pub fn resource_resolver(&self) -> Option<&Arc<dyn ResourceResolver>> {
141        self.resource_resolver.as_ref()
142    }
143
144    // ============================================================================
145    // Compilation
146    // ============================================================================
147
148    /// Compile a FHIRPath expression to a VM plan.
149    ///
150    /// Optionally accepts a base type name for strict validation. When provided
151    /// (e.g., `Some("Patient")`), the compiler will validate that all field accesses
152    /// are valid according to the StructureDefinition.
153    ///
154    /// For explicit (decoupled) strictness and base typing, use `compile_with_options()`.
155    ///
156    /// If the type is not found in the FHIR context or type registry, compilation
157    /// will still proceed but with reduced type validation (the analyzer will use
158    /// a fallback type). This allows compilation to work even with empty contexts
159    /// or unknown types, which is useful for testing or when working without
160    /// full FHIR package definitions.
161    ///
162    /// # Example
163    ///
164    /// ```rust,ignore
165    /// use tlq_fhirpath::Engine;
166    ///
167    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
168    /// let engine = Engine::with_fhir_version("R5").await?;
169    ///
170    /// // Compile without type validation
171    /// let plan1 = engine.compile("Patient.name.given", None)?;
172    ///
173    /// // Compile with type validation
174    /// let plan2 = engine.compile("name.given", Some("Patient"))?;
175    /// # Ok(()) }
176    /// # Ok::<(), Box<dyn std::error::Error>>(())
177    /// ```
178    pub fn compile(&self, expr: &str, base_type: Option<&str>) -> Result<Arc<Plan>> {
179        // Backwards-compatible convenience API: providing a base type implies strict validation.
180        self.compile_with_options(
181            expr,
182            CompileOptions {
183                base_type: base_type.map(|s| s.to_string()),
184                strict: base_type.is_some(),
185            },
186        )
187    }
188
189    /// Compile a FHIRPath expression to a VM plan with explicit options.
190    pub fn compile_with_options(&self, expr: &str, options: CompileOptions) -> Result<Arc<Plan>> {
191        self.compile_internal(expr, &options)
192    }
193
194    fn leading_identifier(expr: &str) -> Option<&str> {
195        let s = expr.trim_start();
196        let mut chars = s.char_indices();
197        let (start, first) = chars.next()?;
198        debug_assert_eq!(start, 0);
199        if !(first.is_ascii_alphabetic() || first == '_') {
200            return None;
201        }
202        let mut end = first.len_utf8();
203        for (idx, c) in chars {
204            if c.is_ascii_alphanumeric() || c == '_' {
205                end = idx + c.len_utf8();
206            } else {
207                break;
208            }
209        }
210        Some(&s[..end])
211    }
212
213    fn resource_type_from_value(resource: &Value) -> Option<String> {
214        match resource.data() {
215            crate::value::ValueData::LazyJson { .. } => match resource.data().resolved_json()? {
216                serde_json::Value::Object(obj) => obj
217                    .get("resourceType")
218                    .and_then(|v| v.as_str())
219                    .map(|s| s.to_string())
220                    .or_else(|| {
221                        crate::vm::infer_structural_root_type_name_from_json(obj)
222                            .map(|s| s.to_string())
223                    }),
224                _ => None,
225            },
226            crate::value::ValueData::Object(obj_map) => obj_map
227                .get("resourceType")
228                .and_then(|col| col.iter().next())
229                .and_then(|value| match value.data() {
230                    crate::value::ValueData::String(rt) => Some(rt.as_ref().to_string()),
231                    _ => None,
232                })
233                .or_else(|| {
234                    crate::vm::infer_structural_root_type_name(obj_map.as_ref())
235                        .map(|s| s.to_string())
236                }),
237            _ => None,
238        }
239    }
240
241    fn infer_compile_base_type_for_eval(
242        &self,
243        expr: &str,
244        ctx: &Context,
245        options: &EvalOptions,
246    ) -> Option<String> {
247        if options.base_type.is_some() || !options.infer_base_type {
248            return options.base_type.clone();
249        }
250
251        let rt = Self::resource_type_from_value(&ctx.resource)?;
252
253        // If the expression already starts with a known FHIR type name, don't lock compilation to
254        // the runtime resource type (typing can be inferred from the expression itself).
255        if let Some(ident) = Self::leading_identifier(expr) {
256            if analyzer::is_fhir_type(&self.fhir_context, ident)
257                || ident.eq_ignore_ascii_case(&rt)
258            {
259                return None;
260            }
261        }
262
263        Some(rt)
264    }
265
266    /// Internal compilation method with explicit options.
267    fn compile_internal(&self, expr: &str, options: &CompileOptions) -> Result<Arc<Plan>> {
268        let cache_key = if options.strict {
269            if let Some(base) = options.base_type.as_deref() {
270                format!("strict:{}::{}", base, expr)
271            } else {
272                format!("strict::{}", expr)
273            }
274        } else {
275            format!("lenient::{}", expr)
276        };
277
278        // Check cache first
279        {
280            let mut cache = self.cache.lock().unwrap();
281            if let Some(plan) = cache.get(&cache_key) {
282                return Ok(plan.clone());
283            }
284        }
285
286        // 1. Parse → AST
287        let mut parser = crate::parser::Parser::new(expr.to_string());
288        let ast = parser.parse()?;
289
290        // Determine a typing/validation base type:
291        // - explicitly provided `base_type`
292        // - otherwise inferred from a leading type name prefix in the expression (e.g., `Patient.name`)
293        let base_type_from_expr = Self::leading_identifier(expr)
294            .filter(|ident| analyzer::is_fhir_type(&self.fhir_context, ident))
295            .map(|s| s.to_string());
296        let typing_base_type = options.base_type.clone().or(base_type_from_expr);
297
298        if options.strict && typing_base_type.is_none() {
299            return Err(Error::TypeError(
300                "Strict compilation requires a base type (provide one or prefix the expression with a root type)".into(),
301            ));
302        }
303
304        // 2. Semantic analysis → HIR (structural only, for now still includes type resolution)
305        let analyzer = Analyzer::new(
306            Arc::clone(&self.type_registry),
307            Arc::clone(&self.function_registry),
308            Arc::clone(&self.variable_registry),
309        );
310        let hir = analyzer.analyze_with_type(ast, typing_base_type.clone())?;
311
312        // 3. Type resolution pass (NEW TWO-PHASE ARCHITECTURE)
313        // For now, this is optional and runs after the analyzer
314        // TODO: Update Analyzer to produce Unknown types, making this pass mandatory
315        let type_pass = crate::typecheck::TypePass::new(
316            Arc::clone(&self.type_registry),
317            Arc::clone(&self.function_registry),
318            Arc::clone(&self.fhir_context),
319        );
320        let hir = type_pass.resolve(hir, typing_base_type, options.strict)?;
321
322        // 4. Generate VM plan
323        let plan = self.codegen(hir)?;
324        let plan = Arc::new(plan);
325
326        // Cache the plan
327        {
328            let mut cache = self.cache.lock().unwrap();
329            cache.put(cache_key, plan.clone());
330        }
331
332        Ok(plan)
333    }
334
335    // ============================================================================
336    // Evaluation
337    // ============================================================================
338
339    /// Evaluate a compiled plan against a context.
340    pub fn evaluate(&self, plan: &Plan, ctx: &Context) -> Result<Collection> {
341        use crate::vm::Vm;
342        let mut vm = Vm::new(ctx, self);
343        vm.execute(plan)
344    }
345
346    /// Evaluate a compiled plan against multiple JSON string resources in batch.
347    ///
348    /// OPTIMIZED: Accepts JSON strings directly to avoid double serialization.
349    /// This is highly optimized for bulk operations:
350    /// - Single FFI boundary crossing instead of N calls
351    /// - Avoids JSON serialization overhead (resources already strings)
352    /// - Tight loop in Rust for better CPU cache utilization
353    ///
354    /// # Example
355    ///
356    /// ```rust,ignore
357    /// let plan = engine.compile("Patient.name.given")?;
358    /// let json_strings = vec!["{\"resourceType\":\"Patient\"...}", ...];
359    /// let results = engine.evaluate_batch(&plan, &json_strings)?;
360    /// ```
361    pub fn evaluate_batch(&self, plan: &Plan, json_strings: &[&str]) -> Result<Vec<Collection>> {
362        use crate::vm::Vm;
363
364        // Pre-allocate result vector
365        let mut results = Vec::with_capacity(json_strings.len());
366
367        // Pre-parse all JSON strings to avoid repeated parsing overhead
368        let mut parsed_resources = Vec::with_capacity(json_strings.len());
369        for json_str in json_strings {
370            let json_value: serde_json::Value = serde_json::from_str(json_str)
371                .map_err(|e| Error::EvaluationError(format!("Invalid JSON: {}", e)))?;
372            parsed_resources.push(json_value);
373        }
374
375        // Evaluate each resource in a tight loop
376        // This stays in Rust, avoiding FFI overhead for each resource
377        for resource in parsed_resources {
378            let root = Value::from_json(resource);
379            let ctx = Context::new(root);
380            let mut vm = Vm::new(&ctx, self);
381            let collection = vm.execute(plan)?;
382            results.push(collection);
383        }
384
385        Ok(results)
386    }
387
388    /// Evaluate an expression directly (compile + evaluate).
389    ///
390    /// Optionally accepts a base type name for strict validation during compilation.
391    ///
392    /// For explicit (decoupled) strictness and base typing, use `evaluate_expr_with_options()`.
393    ///
394    /// # Example
395    ///
396    /// ```rust,ignore
397    /// use tlq_fhirpath::{Engine, Context, Value};
398    ///
399    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
400    /// let engine = Engine::with_fhir_version("R5").await?;
401    /// let ctx = Context::new(Value::empty());
402    ///
403    /// // Evaluate without type validation
404    /// let result1 = engine.evaluate_expr("1 + 2", &ctx, None)?;
405    ///
406    /// // Evaluate with type validation
407    /// let result2 = engine.evaluate_expr("name.given", &ctx, Some("Patient"))?;
408    /// # Ok(()) }
409    /// # Ok::<(), Box<dyn std::error::Error>>(())
410    /// ```
411    pub fn evaluate_expr(
412        &self,
413        expr: &str,
414        ctx: &Context,
415        base_type: Option<&str>,
416    ) -> Result<Collection> {
417        // Backwards-compatible convenience API: providing a base type implies strict validation.
418        self.evaluate_expr_with_options(
419            expr,
420            ctx,
421            EvalOptions {
422                base_type: base_type.map(|s| s.to_string()),
423                strict: base_type.is_some(),
424                infer_base_type: true,
425            },
426        )
427    }
428
429    /// Evaluate an expression directly (compile + evaluate) with explicit options.
430    pub fn evaluate_expr_with_options(
431        &self,
432        expr: &str,
433        ctx: &Context,
434        options: EvalOptions,
435    ) -> Result<Collection> {
436        let inferred_base = self.infer_compile_base_type_for_eval(expr, ctx, &options);
437        let plan = self.compile_with_options(
438            expr,
439            CompileOptions {
440                base_type: inferred_base.or(options.base_type),
441                strict: options.strict,
442            },
443        )?;
444        self.evaluate(&plan, ctx)
445    }
446
447    /// Evaluate an expression against a JSON resource.
448    ///
449    /// Optionally accepts a base type name for strict validation during compilation.
450    ///
451    /// # Example
452    ///
453    /// ```rust,ignore
454    /// use tlq_fhirpath::Engine;
455    /// use serde_json::json;
456    ///
457    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
458    /// let engine = Engine::with_fhir_version("R5").await?;
459    /// let resource = json!({"resourceType": "Patient", "name": [{"given": ["John"]}]});
460    ///
461    /// // Evaluate without type validation
462    /// let result1 = engine.evaluate_json("Patient.name.given", &resource, None)?;
463    ///
464    /// // Evaluate with type validation
465    /// let result2 = engine.evaluate_json("name.given", &resource, Some("Patient"))?;
466    /// # Ok(()) }
467    /// # Ok::<(), Box<dyn std::error::Error>>(())
468    /// ```
469    pub fn evaluate_json(
470        &self,
471        expr: &str,
472        resource: serde_json::Value,
473        base_type: Option<&str>,
474    ) -> Result<Collection> {
475        let root = Value::from_json(resource);
476        let ctx = Context::new(root);
477        self.evaluate_expr(expr, &ctx, base_type)
478    }
479
480    /// Evaluate an expression against an XML resource string.
481    ///
482    /// This method converts the XML resource to JSON internally before evaluation.
483    /// The XML must be a valid FHIR resource in XML format.
484    ///
485    /// Optionally accepts a base type name for strict validation during compilation.
486    ///
487    /// # Example
488    ///
489    /// ```rust,ignore
490    /// use tlq_fhirpath::Engine;
491    ///
492    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
493    /// let engine = Engine::with_fhir_version("R5").await?;
494    /// let xml = r#"<Patient xmlns="http://hl7.org/fhir">
495    ///     <id value="pat-1"/>
496    ///     <active value="true"/>
497    /// </Patient>"#;
498    ///
499    /// // Evaluate without type validation
500    /// let result1 = engine.evaluate_xml("Patient.active", xml, None)?;
501    ///
502    /// // Evaluate with type validation
503    /// let result2 = engine.evaluate_xml("active", xml, Some("Patient"))?;
504    /// # Ok(()) }
505    /// # Ok::<(), Box<dyn std::error::Error>>(())
506    /// ```
507    #[cfg(feature = "xml-support")]
508    pub fn evaluate_xml(
509        &self,
510        expr: &str,
511        xml_resource: &str,
512        base_type: Option<&str>,
513    ) -> Result<Collection> {
514        let json_str = fhir_format::xml_to_json(xml_resource)
515            .map_err(|e| Error::EvaluationError(format!("XML parse error: {}", e)))?;
516        let resource: serde_json::Value = serde_json::from_str(&json_str)
517            .map_err(|e| Error::EvaluationError(format!("JSON parse error: {}", e)))?;
518        self.evaluate_json(expr, resource, base_type)
519    }
520
521    /// Evaluate an expression against a Value.
522    ///
523    /// Optionally accepts a base type name for strict validation during compilation.
524    ///
525    /// # Example
526    ///
527    /// ```rust,ignore
528    /// use tlq_fhirpath::{Engine, Value};
529    ///
530    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
531    /// let engine = Engine::with_fhir_version("R5").await?;
532    /// let resource = Value::empty();
533    ///
534    /// // Evaluate without type validation
535    /// let result1 = engine.evaluate_value("1 + 2", resource.clone(), None)?;
536    ///
537    /// // Evaluate with type validation
538    /// let result2 = engine.evaluate_value("name.given", resource, Some("Patient"))?;
539    /// # Ok(()) }
540    /// # Ok::<(), Box<dyn std::error::Error>>(())
541    /// ```
542    pub fn evaluate_value(
543        &self,
544        expr: &str,
545        resource: Value,
546        base_type: Option<&str>,
547    ) -> Result<Collection> {
548        let ctx = Context::new(resource);
549        self.evaluate_expr(expr, &ctx, base_type)
550    }
551
552    /// Check if a type name is a FHIR type (not a System type)
553    pub fn is_fhir_type(&self, type_name: &str) -> bool {
554        analyzer::is_fhir_type(&self.fhir_context, type_name)
555    }
556
557    // ============================================================================
558    // Visualization
559    // ============================================================================
560
561    /// Visualize the compilation pipeline for an expression
562    ///
563    /// Returns AST, HIR, and VM Plan visualizations in the specified format.
564    ///
565    /// # Example
566    ///
567    /// ```rust,ignore
568    /// use fhirpath_engine::{Engine, visualize::VisualizationFormat};
569    ///
570    /// let engine = Engine::with_empty_context();
571    /// let viz = engine.visualize_pipeline("Patient.name", VisualizationFormat::AsciiTree)?;
572    /// println!("AST:\n{}", viz.ast);
573    /// println!("HIR:\n{}", viz.hir);
574    /// println!("Plan:\n{}", viz.plan);
575    /// # Ok::<(), Box<dyn std::error::Error>>(())
576    /// ```
577    pub fn visualize_pipeline(
578        &self,
579        expr: &str,
580        format: crate::visualize::VisualizationFormat,
581    ) -> Result<PipelineVisualization> {
582        use crate::visualize::Visualize;
583
584        // 1. Parse → AST
585        let mut parser = crate::parser::Parser::new(expr.to_string());
586        let ast = parser.parse()?;
587        let ast_viz = ast.visualize(format);
588
589        // 2. Analyze → HIR
590        let analyzer = Analyzer::new(
591            Arc::clone(&self.type_registry),
592            Arc::clone(&self.function_registry),
593            Arc::clone(&self.variable_registry),
594        );
595        let hir = analyzer.analyze_with_type(ast, None)?;
596
597        // 3. Type resolution
598        let type_pass = crate::typecheck::TypePass::new(
599            Arc::clone(&self.type_registry),
600            Arc::clone(&self.function_registry),
601            Arc::clone(&self.fhir_context),
602        );
603        let hir = type_pass.resolve(hir, None, false)?;
604        let hir_viz = hir.visualize(format);
605
606        // 4. Codegen
607        let plan = self.codegen(hir)?;
608        let plan_viz = plan.visualize(format);
609
610        Ok(PipelineVisualization {
611            ast: ast_viz,
612            hir: hir_viz,
613            plan: plan_viz,
614        })
615    }
616
617    /// Visualize just the AST for an expression
618    pub fn visualize_ast(
619        &self,
620        expr: &str,
621        format: crate::visualize::VisualizationFormat,
622    ) -> Result<String> {
623        use crate::visualize::Visualize;
624        let mut parser = crate::parser::Parser::new(expr.to_string());
625        let ast = parser.parse()?;
626        Ok(ast.visualize(format))
627    }
628
629    /// Visualize just the HIR for an expression
630    pub fn visualize_hir(
631        &self,
632        expr: &str,
633        format: crate::visualize::VisualizationFormat,
634    ) -> Result<String> {
635        use crate::visualize::Visualize;
636        let mut parser = crate::parser::Parser::new(expr.to_string());
637        let ast = parser.parse()?;
638        let analyzer = Analyzer::new(
639            Arc::clone(&self.type_registry),
640            Arc::clone(&self.function_registry),
641            Arc::clone(&self.variable_registry),
642        );
643        let hir = analyzer.analyze_with_type(ast, None)?;
644        let type_pass = crate::typecheck::TypePass::new(
645            Arc::clone(&self.type_registry),
646            Arc::clone(&self.function_registry),
647            Arc::clone(&self.fhir_context),
648        );
649        let hir = type_pass.resolve(hir, None, false)?;
650        Ok(hir.visualize(format))
651    }
652
653    /// Visualize just the VM Plan for an expression
654    pub fn visualize_plan(
655        &self,
656        expr: &str,
657        format: crate::visualize::VisualizationFormat,
658    ) -> Result<String> {
659        use crate::visualize::Visualize;
660        let plan = self.compile(expr, None)?;
661        Ok(plan.visualize(format))
662    }
663
664    /// Code generation: HIR → VM Plan
665    fn codegen(&self, hir: crate::hir::HirNode) -> Result<Plan> {
666        let mut codegen = CodeGenerator::new();
667        codegen.generate(hir)?;
668        Ok(codegen.build())
669    }
670}
671
672/// Result of visualizing the entire compilation pipeline
673#[derive(Debug, Clone)]
674pub struct PipelineVisualization {
675    /// AST visualization
676    pub ast: String,
677    /// HIR visualization
678    pub hir: String,
679    /// VM Plan visualization
680    pub plan: String,
681}