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}