Skip to main content

tenferro_runtime/graph/
compiler.rs

1use std::collections::HashMap;
2use std::fmt;
3use std::sync::Arc;
4
5use computegraph::compile::{compile, CompiledProgram};
6use computegraph::materialize::{
7    materialize_merge, MaterializedGraph, MaterializedOperation, MaterializedValue,
8};
9use computegraph::resolve::{resolve, ResolvedView, ValueDef};
10use computegraph::types::{OperationKey, ValueKey};
11use computegraph::GraphOperation;
12use num_complex::{Complex32, Complex64};
13use tenferro_ops::dim_expr::{DimExpr, DimExprEvalError};
14use tenferro_ops::input_key::TensorInputKey;
15use tenferro_ops::std_tensor_op::StdTensorOp;
16use tenferro_ops::{ShapeExtent, ShapeRelation, SymDim};
17use tenferro_tensor::{CacheStats, DType, SliceConfig, Tensor, TensorScalar};
18
19use super::program::CompiledGraph;
20#[cfg(test)]
21use crate::compiler::semantic_staging::stage_semantic_program;
22use crate::compiler::{lower_scoped_dim_expr, CompilerOptions};
23use crate::error::{Error, Result};
24use crate::extension_cache::{ExtensionCacheSelector, ExtensionCacheStore};
25use crate::metadata::registered_meta;
26use crate::program::{
27    CoreSemanticOp, FrozenProgram, ProgramInputSpec, ProgramShapeRelation, ProgramValueMetadata,
28    SemanticOpRef, SemanticProgramBuilder, ShapeGuard as ProgramShapeGuard,
29};
30use crate::shape_constraint::{discharge, LocalShapeConstraint, SlotScopedShapeConstraint};
31use crate::shape_infer::{infer_extension_output_meta, infer_output_shapes};
32use crate::trace::TracedGraph;
33use crate::traced::{try_concrete_shape, TracedTensor};
34
35#[derive(Clone)]
36struct InputDescriptor {
37    dtype: DType,
38    shape: Vec<usize>,
39    extent_identity: InputExtentIdentity,
40    default_tensor: Option<Arc<Tensor>>,
41}
42
43#[derive(Clone, Copy)]
44enum InputExtentIdentity {
45    Concrete,
46    Symbolic,
47}
48
49impl InputDescriptor {
50    fn semantic_shape(&self, input_idx: usize) -> Vec<DimExpr> {
51        match self.extent_identity {
52            InputExtentIdentity::Concrete => DimExpr::from_concrete(&self.shape),
53            InputExtentIdentity::Symbolic => (0..self.shape.len())
54                .map(|axis| DimExpr::InputDim { input_idx, axis })
55                .collect(),
56        }
57    }
58
59    fn constraint_guard_shape(&self, input_idx: usize) -> Vec<DimExpr> {
60        if self.default_tensor.is_some() {
61            return input_dim_shape(input_idx, self.shape.len());
62        }
63        self.semantic_shape(input_idx)
64    }
65}
66
67fn input_dim_shape(input_idx: usize, rank: usize) -> Vec<DimExpr> {
68    (0..rank)
69        .map(|axis| DimExpr::InputDim { input_idx, axis })
70        .collect()
71}
72
73/// Compiler for traced tensor graphs.
74///
75/// A graph compiler lowers one or more [`TracedTensor`] outputs to a reusable
76/// [`CompiledGraph`] without requiring a backend.
77///
78/// # Examples
79///
80/// ```
81/// use tenferro_runtime::{GraphCompiler, TracedTensor};
82///
83/// let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
84/// let y = (&x + &x).unwrap();
85/// let mut compiler = GraphCompiler::new();
86/// let program = compiler.compile(&y).unwrap();
87/// assert_eq!(program.output_count(), 1);
88/// ```
89pub struct GraphCompiler {
90    extension_cache: ExtensionCacheStore,
91    compiler_options: CompilerOptions,
92}
93
94impl fmt::Debug for GraphCompiler {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        f.debug_struct("GraphCompiler")
97            .field("extension_cache_stats", &self.cache_stats())
98            .field("compiler_options", &self.compiler_options)
99            .field("extension_cache", &self.extension_cache)
100            .finish_non_exhaustive()
101    }
102}
103
104impl GraphCompiler {
105    /// Create a compiler with bounded default caches.
106    ///
107    /// # Examples
108    ///
109    /// ```
110    /// use tenferro_runtime::GraphCompiler;
111    ///
112    /// let compiler = GraphCompiler::new();
113    /// assert!(compiler.extension_caches().is_empty());
114    /// ```
115    pub fn new() -> Self {
116        Self {
117            extension_cache: ExtensionCacheStore::new(),
118            compiler_options: CompilerOptions::default(),
119        }
120    }
121
122    /// Create a compiler with explicit lowering and optimizer options.
123    ///
124    /// # Examples
125    ///
126    /// ```
127    /// use tenferro_runtime::{CompilerOptions, OptimizerConfig};
128    /// use tenferro_runtime::GraphCompiler;
129    ///
130    /// let compiler = GraphCompiler::with_compiler_options(CompilerOptions {
131    ///     optimizer: OptimizerConfig {
132    ///         dot_decomposer: true,
133    ///         ..OptimizerConfig::default()
134    ///     },
135    /// });
136    /// assert!(compiler.compiler_options().optimizer.dot_decomposer);
137    /// ```
138    pub fn with_compiler_options(compiler_options: CompilerOptions) -> Self {
139        Self {
140            extension_cache: ExtensionCacheStore::new(),
141            compiler_options,
142        }
143    }
144
145    /// Compile one traced output into a graph program.
146    ///
147    /// # Examples
148    ///
149    /// ```
150    /// use tenferro_runtime::{GraphCompiler, TracedTensor};
151    ///
152    /// let x = TracedTensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
153    /// let mut compiler = GraphCompiler::new();
154    /// let y = x.neg().unwrap();
155    /// let program = compiler.compile(&y).unwrap();
156    /// assert_eq!(program.input_count(), 1);
157    /// ```
158    ///
159    /// # Errors
160    ///
161    /// Returns [`Error::Validation`] with `ShapeMismatch`, `RankMismatch`,
162    /// `DTypeMismatch`, or `InvalidArgument` for invalid graph metadata or
163    /// shape constraints, [`Error::RuntimeState`] for missing/inconsistent
164    /// metadata or cache state, and [`Error::Internal`] when the graph
165    /// violates a compiler invariant. Extension lowering failures retain
166    /// their typed [`Error::Extension`] source.
167    pub fn compile(&mut self, output: &TracedTensor) -> Result<CompiledGraph> {
168        self.compile_many(&[output])
169    }
170
171    /// Compile an immutable semantic trace without consulting a backend.
172    ///
173    /// This is the forward-only trace boundary. The compiler preserves the
174    /// frozen semantic program and bindings without preparing backend/runtime
175    /// staging. Runtime preparation owns backend-private staging and plan caches.
176    ///
177    /// # Errors
178    ///
179    /// Returns [`Error::Validation`] for invalid metadata or shape constraints,
180    /// [`Error::Extension`] when extension lowering fails,
181    /// [`Error::RuntimeState`] for inconsistent staging state, or
182    /// [`Error::Internal`] when compilation encounters an invariant violation.
183    pub fn compile_traced_graph(&mut self, graph: &TracedGraph) -> Result<CompiledGraph> {
184        self.compile_frozen(graph.frozen())
185    }
186
187    /// Compile an immutable semantic program for ordered execution.
188    ///
189    /// This entry is used by validation-preserving semantic transforms such as
190    /// whole-program AD. Tensor bindings remain outside semantic identity and
191    /// are preserved in the returned [`CompiledGraph`].
192    ///
193    /// # Examples
194    ///
195    /// ```
196    /// use tenferro_ops::dim_expr::DimExpr;
197    /// use tenferro_runtime::program::{
198    ///     CoreSemanticOp, ProgramInputSpec, SemanticProgramBuilder,
199    /// };
200    /// use tenferro_runtime::{DType, GraphCompiler};
201    ///
202    /// let mut builder = SemanticProgramBuilder::new();
203    /// let input = builder
204    ///     .input(ProgramInputSpec::new(DType::F64, [DimExpr::Const(2)]))
205    ///     .unwrap();
206    /// let output = builder.add_op(CoreSemanticOp::Neg, &[input]).unwrap()[0];
207    /// let frozen = builder.finish(&[output]).unwrap();
208    /// let compiled = GraphCompiler::new()
209    ///     .compile_frozen_program(&frozen)
210    ///     .unwrap();
211    /// assert_eq!(compiled.input_count(), 1);
212    /// ```
213    ///
214    /// # Errors
215    ///
216    /// Returns [`Error::Validation`] for invalid metadata or shape constraints,
217    /// [`Error::Extension`] when extension lowering fails,
218    /// [`Error::RuntimeState`] for inconsistent staging state, or
219    /// [`Error::Internal`] when compilation encounters an invariant violation.
220    pub fn compile_frozen_program(&mut self, frozen: &FrozenProgram) -> Result<CompiledGraph> {
221        self.compile_frozen(frozen)
222    }
223
224    fn compile_frozen(&mut self, frozen: &FrozenProgram) -> Result<CompiledGraph> {
225        validate_bound_shape_guards(frozen)?;
226        Ok(CompiledGraph::new(
227            frozen.clone(),
228            self.compiler_options,
229            [],
230        ))
231    }
232
233    /// Compile multiple traced outputs into one graph program.
234    ///
235    /// # Examples
236    ///
237    /// ```
238    /// use tenferro_runtime::{GraphCompiler, TracedTensor};
239    ///
240    /// let x = TracedTensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
241    /// let y = x.neg().unwrap();
242    /// let mut compiler = GraphCompiler::new();
243    /// let program = compiler.compile_many(&[&x, &y]).unwrap();
244    /// assert_eq!(program.output_count(), 2);
245    /// ```
246    ///
247    /// # Errors
248    ///
249    /// Returns [`Error::Validation`] with `ShapeMismatch`, `RankMismatch`,
250    /// `DTypeMismatch`, or `InvalidArgument` for invalid graph metadata or
251    /// shape constraints, [`Error::RuntimeState`] for missing/inconsistent
252    /// metadata or cache state, and [`Error::Internal`] when the graph
253    /// violates a compiler invariant. Extension lowering failures retain
254    /// their typed [`Error::Extension`] source.
255    pub fn compile_many(&mut self, outputs: &[&TracedTensor]) -> Result<CompiledGraph> {
256        let all_inputs = collect_default_inputs(outputs)?;
257        self.compile_many_with_descriptors(
258            outputs,
259            &HashMap::new(),
260            &all_inputs,
261            None,
262            false,
263            false,
264        )
265    }
266
267    pub(crate) fn compile_ad_source(&mut self, output: &TracedTensor) -> Result<CompiledGraph> {
268        let all_inputs = collect_default_inputs(&[output])?;
269        self.compile_many_with_descriptors(
270            &[output],
271            &HashMap::new(),
272            &all_inputs,
273            None,
274            true,
275            true,
276        )
277    }
278
279    /// Compile one traced output with concrete placeholder specs.
280    ///
281    /// # Examples
282    ///
283    /// ```
284    /// use tenferro_runtime::{DType, GraphCompiler, TracedTensor};
285    ///
286    /// let x = TracedTensor::input_symbolic_shape(DType::F64, 1).unwrap();
287    /// let mut compiler = GraphCompiler::new();
288    /// let y = x.neg().unwrap();
289    /// let program = compiler
290    ///     .compile_with_input_specs(&y, &[(&x, DType::F64, &[3])])
291    ///     .unwrap();
292    /// assert_eq!(program.input_count(), 1);
293    /// ```
294    ///
295    /// # Errors
296    ///
297    /// Returns [`Error::UnexpectedBinding`] for a data-carrying tensor,
298    /// [`Error::DuplicateBinding`] for repeated placeholders,
299    /// [`Error::PlaceholderDtypeMismatch`],
300    /// [`Error::PlaceholderShapeMismatch`], or
301    /// [`Error::PlaceholderRankMismatch`] for incompatible specs, and
302    /// [`Error::Validation`] with `ShapeMismatch`, `RankMismatch`,
303    /// `DTypeMismatch`, or `InvalidArgument` / [`Error::RuntimeState`] when
304    /// compilation or metadata lowering fails.
305    pub fn compile_with_input_specs(
306        &mut self,
307        output: &TracedTensor,
308        bindings: &[(&TracedTensor, DType, &[usize])],
309    ) -> Result<CompiledGraph> {
310        let mut binding_specs = HashMap::new();
311        let mut input_order = Vec::with_capacity(bindings.len());
312        for (index, (placeholder, dtype, shape)) in bindings.iter().enumerate() {
313            validate_placeholder_spec(index, placeholder, *dtype, shape)?;
314            let key = placeholder.input_key().ok_or(Error::UnexpectedBinding {
315                binding_index: index,
316            })?;
317            if binding_specs
318                .insert(
319                    key.clone(),
320                    InputDescriptor {
321                        dtype: *dtype,
322                        shape: (*shape).to_vec(),
323                        extent_identity: InputExtentIdentity::Concrete,
324                        default_tensor: None,
325                    },
326                )
327                .is_some()
328            {
329                return Err(Error::DuplicateBinding {
330                    input_key: format!("{:?}", key),
331                });
332            }
333            input_order.push(key);
334        }
335
336        self.compile_many_with_descriptors(
337            &[output],
338            &binding_specs,
339            output.inputs_map.as_ref(),
340            Some(&input_order),
341            false,
342            false,
343        )
344    }
345
346    /// Return the compiler options used for future graph lowerings.
347    ///
348    /// # Examples
349    ///
350    /// ```
351    /// use tenferro_runtime::CompilerOptions;
352    /// use tenferro_runtime::GraphCompiler;
353    ///
354    /// let compiler = GraphCompiler::new();
355    /// assert_eq!(compiler.compiler_options(), CompilerOptions::default());
356    /// ```
357    pub fn compiler_options(&self) -> CompilerOptions {
358        self.compiler_options
359    }
360
361    /// Replace compiler options and clear compiler-owned extension cache entries.
362    ///
363    /// # Examples
364    ///
365    /// ```
366    /// use tenferro_runtime::{CompilerOptions, OptimizerConfig};
367    /// use tenferro_runtime::GraphCompiler;
368    ///
369    /// let mut compiler = GraphCompiler::new();
370    /// let options = CompilerOptions {
371    ///     optimizer: OptimizerConfig {
372    ///         dot_decomposer: true,
373    ///         ..OptimizerConfig::default()
374    ///     },
375    /// };
376    /// compiler.set_compiler_options(options);
377    /// assert_eq!(compiler.compiler_options(), options);
378    /// assert_eq!(compiler.cache_stats().entries, 0);
379    /// ```
380    pub fn set_compiler_options(&mut self, compiler_options: CompilerOptions) {
381        if self.compiler_options == compiler_options {
382            return;
383        }
384        self.compiler_options = compiler_options;
385        self.clear_extension_caches();
386    }
387
388    /// Clear generic extension compile-time cache entries.
389    ///
390    /// # Examples
391    ///
392    /// ```
393    /// use tenferro_runtime::GraphCompiler;
394    ///
395    /// let mut compiler = GraphCompiler::new();
396    /// compiler.clear_extension_caches();
397    /// assert_eq!(compiler.cache_stats().entries, 0);
398    /// ```
399    pub fn clear_extension_caches(&mut self) {
400        self.extension_cache.clear();
401    }
402
403    /// Clear every cache owned by the compiler.
404    ///
405    /// # Examples
406    ///
407    /// ```
408    /// use tenferro_runtime::GraphCompiler;
409    ///
410    /// let mut compiler = GraphCompiler::new();
411    /// compiler.clear_caches();
412    /// assert_eq!(compiler.cache_stats().entries, 0);
413    /// ```
414    pub fn clear_caches(&mut self) {
415        self.clear_extension_caches();
416    }
417
418    /// Return compiler-owned extension cache-entry and retained-byte stats.
419    ///
420    /// # Examples
421    ///
422    /// ```
423    /// use tenferro_runtime::GraphCompiler;
424    ///
425    /// let compiler = GraphCompiler::new();
426    /// let stats = compiler.cache_stats();
427    /// assert_eq!(stats.entries, 0);
428    /// ```
429    pub fn cache_stats(&self) -> CacheStats {
430        self.extension_cache.stats(ExtensionCacheSelector::All)
431    }
432
433    /// Borrow generic compiler-owned extension cache storage.
434    ///
435    /// # Examples
436    ///
437    /// ```
438    /// use tenferro_runtime::GraphCompiler;
439    ///
440    /// let compiler = GraphCompiler::new();
441    /// assert!(compiler.extension_caches().is_empty());
442    /// ```
443    pub fn extension_caches(&self) -> &ExtensionCacheStore {
444        &self.extension_cache
445    }
446
447    /// Mutably borrow generic compiler-owned extension cache storage.
448    ///
449    /// # Examples
450    ///
451    /// ```
452    /// use tenferro_runtime::GraphCompiler;
453    ///
454    /// let mut compiler = GraphCompiler::new();
455    /// compiler.extension_caches_mut().clear();
456    /// ```
457    pub fn extension_caches_mut(&mut self) -> &mut ExtensionCacheStore {
458        &mut self.extension_cache
459    }
460
461    fn compile_many_with_descriptors(
462        &mut self,
463        outputs: &[&TracedTensor],
464        binding_specs: &HashMap<TensorInputKey, InputDescriptor>,
465        default_inputs: &HashMap<TensorInputKey, Arc<Tensor>>,
466        explicit_input_order: Option<&[TensorInputKey]>,
467        include_checkpoint_aliases: bool,
468        allow_unbound_placeholders: bool,
469    ) -> Result<CompiledGraph> {
470        let mut constraint_scopes = Vec::new();
471        let mut seen_constraint_scopes = std::collections::HashSet::new();
472        for output in outputs {
473            for scope in output.constraint_scopes.as_slice() {
474                if seen_constraint_scopes.insert(Arc::as_ptr(scope)) {
475                    #[cfg(test)]
476                    test_support::record_constraint_scope_clones(1);
477                    constraint_scopes.push(Arc::clone(scope));
478                }
479            }
480        }
481
482        let mut roots = Vec::new();
483        let mut checkpoint_aliases = HashMap::new();
484        let mut output_keys = Vec::with_capacity(outputs.len());
485        for output in outputs {
486            roots.extend(output.resolve_roots());
487            if include_checkpoint_aliases {
488                if let Some(chain) = &output.checkpoint_chain {
489                    roots.extend(chain.collect_graphs());
490                    for (alias_key, target_key) in chain.collect_aliases() {
491                        insert_checkpoint_alias(&mut checkpoint_aliases, alias_key, target_key)?;
492                    }
493                }
494            }
495            output_keys.push(output.graph.values()[output.val].key.clone());
496        }
497
498        let view = resolve(roots);
499        let graph = if checkpoint_aliases.is_empty() {
500            materialize_merge(&view, &output_keys)
501        } else {
502            let checkpoint_alias_shapes = checkpoint_aliases
503                .keys()
504                .filter_map(|key| {
505                    default_inputs
506                        .get(key)
507                        .map(|tensor| (key.clone(), tensor.shape().to_vec()))
508                })
509                .collect::<HashMap<_, _>>();
510            materialize_merge_with_input_aliases(
511                &view,
512                &output_keys,
513                &checkpoint_aliases,
514                &checkpoint_alias_shapes,
515            )
516        };
517        let mut compiled = compile(&graph);
518        prune_compiled_extension_outputs(&mut compiled)?;
519        let slot_by_key: HashMap<_, _> = graph
520            .values
521            .iter()
522            .enumerate()
523            .map(|(slot, value)| (value.key.clone(), slot))
524            .collect();
525        let mut scoped_constraints = Vec::new();
526        for scope in constraint_scopes {
527            for scoped in scope.constraints() {
528                let origin_slots: Vec<_> = scoped
529                    .origins
530                    .iter()
531                    .filter_map(|key| slot_by_key.get(key).copied())
532                    .collect();
533                if origin_slots.is_empty() {
534                    continue;
535                }
536                let origin_instruction = origin_slots.iter().find_map(|&slot| {
537                    graph
538                        .values
539                        .get(slot)
540                        .and_then(|value| value.producer.map(|p| p.0))
541                });
542                let mut local = scoped.local.clone();
543                if let Some(instruction_index) = origin_instruction {
544                    local.source = local.source.with_instruction(instruction_index);
545                }
546                let mut input_slots = Vec::with_capacity(scoped.inputs.len());
547                for (input_idx, key) in scoped.inputs.iter().enumerate() {
548                    let Some(slot) = slot_by_key.get(key).copied() else {
549                        return Err(Error::ShapeConstraintEvaluation {
550                            family: local.source.family_id,
551                            instruction_index: local.source.instruction_index,
552                            relation: local.relation,
553                            expression: format!("{:?}", local.lhs),
554                            cause: crate::ShapeConstraintEvalError::MissingInput {
555                                input_idx,
556                                input_count: scoped.inputs.len(),
557                            },
558                        });
559                    };
560                    input_slots.push(slot);
561                }
562                scoped_constraints.push(SlotScopedShapeConstraint {
563                    origin_slots,
564                    input_slots,
565                    local,
566                });
567            }
568        }
569
570        let mut descriptors = Vec::with_capacity(graph.inputs.len());
571        let mut input_keys = Vec::with_capacity(graph.inputs.len());
572        for key in &graph.inputs {
573            let ValueKey::Input(input_key) = key else {
574                return Err(Error::Internal(
575                    "expected Input key in graph inputs".to_string(),
576                ));
577            };
578            let descriptor = descriptor_for_input(
579                input_key,
580                binding_specs,
581                default_inputs,
582                allow_unbound_placeholders,
583            )?;
584            descriptors.push(descriptor);
585            input_keys.push(input_key.clone());
586        }
587        if let Some(explicit_input_order) = explicit_input_order {
588            let input_position_by_key: HashMap<_, _> = graph
589                .inputs
590                .iter()
591                .enumerate()
592                .filter_map(|(position, key)| match key {
593                    ValueKey::Input(key) => Some((key.clone(), position)),
594                    _ => None,
595                })
596                .collect();
597            let mut ordered_positions = Vec::with_capacity(graph.inputs.len());
598            let mut selected_positions = vec![false; graph.inputs.len()];
599            for key in explicit_input_order {
600                if let Some(&position) = input_position_by_key.get(key) {
601                    ordered_positions.push(position);
602                    selected_positions[position] = true;
603                }
604            }
605            for (position, selected) in selected_positions.iter().enumerate() {
606                if !selected {
607                    ordered_positions.push(position);
608                }
609            }
610            compiled.input_slots = ordered_positions
611                .iter()
612                .map(|&position| compiled.input_slots[position])
613                .collect();
614            descriptors = ordered_positions
615                .iter()
616                .map(|&position| descriptors[position].clone())
617                .collect();
618            input_keys = ordered_positions
619                .into_iter()
620                .map(|position| input_keys[position].clone())
621                .collect();
622        }
623
624        let semantic =
625            compile_materialized_semantic_program(&compiled, &descriptors, &scoped_constraints)?;
626        validate_bound_shape_guards(&semantic)?;
627        Ok(CompiledGraph::new(
628            semantic,
629            self.compiler_options,
630            input_keys,
631        ))
632    }
633}
634
635fn validate_bound_shape_guards(frozen: &FrozenProgram) -> Result<()> {
636    let input_shapes = compile_time_input_shapes(frozen)?;
637    for operation in frozen.program.operations() {
638        let fallback_family = match operation.op() {
639            SemanticOpRef::Core(_) => "tenferro-runtime.core.v1",
640            SemanticOpRef::Extension(extension) => extension.family_id(),
641        };
642        for guard in operation.shape_guards() {
643            if guard.source_family().is_some() {
644                continue;
645            }
646            validate_bound_shape_guard(guard, fallback_family, &input_shapes)?;
647        }
648    }
649    Ok(())
650}
651
652fn compile_time_input_shapes(frozen: &FrozenProgram) -> Result<Vec<Option<Vec<usize>>>> {
653    let metadata = frozen.input_metadata_with_bound_shapes();
654    frozen
655        .program
656        .inputs()
657        .iter()
658        .enumerate()
659        .map(|(input_idx, &input)| {
660            if let Some(tensor) = frozen.bindings.tensor_ref_for_input(input) {
661                return Ok(Some(tensor.shape().to_vec()));
662            }
663            let Some(metadata) = metadata.get(input_idx) else {
664                return Err(invalid_compiled_graph(format!(
665                    "semantic input metadata index {input_idx} is outside metadata table"
666                )));
667            };
668            concrete_shape_from_input_metadata(metadata)
669        })
670        .collect()
671}
672
673fn concrete_shape_from_input_metadata(
674    metadata: &ProgramValueMetadata,
675) -> Result<Option<Vec<usize>>> {
676    let mut shape = Vec::with_capacity(metadata.shape().len());
677    for extent in metadata.shape() {
678        let ShapeExtent::Exact(expression) = extent else {
679            return Ok(None);
680        };
681        let Some(value) = evaluate_static_dim_expr(expression)? else {
682            return Ok(None);
683        };
684        shape.push(value);
685    }
686    Ok(Some(shape))
687}
688
689fn validate_bound_shape_guard(
690    guard: &ProgramShapeGuard,
691    fallback_family: &'static str,
692    input_shapes: &[Option<Vec<usize>>],
693) -> Result<()> {
694    let ProgramShapeRelation::Equal = guard.relation() else {
695        return Ok(());
696    };
697    let family = guard.source_family().unwrap_or(fallback_family);
698    let relation = ShapeRelation::Equal;
699    let lhs = evaluate_bound_shape_guard_expression(family, relation, guard.lhs(), input_shapes)?;
700    let rhs = evaluate_bound_shape_guard_expression(family, relation, guard.rhs(), input_shapes)?;
701    let (Some(lhs), Some(rhs)) = (lhs, rhs) else {
702        return Ok(());
703    };
704    if lhs == rhs {
705        return Ok(());
706    }
707    Err(Error::ShapeConstraintViolation {
708        family,
709        instruction_index: None,
710        relation,
711        lhs_expr: format!("{:?}", guard.lhs()),
712        rhs_expr: format!("{:?}", guard.rhs()),
713        lhs_value: lhs,
714        rhs_value: rhs,
715    })
716}
717
718fn evaluate_bound_shape_guard_expression(
719    family: &'static str,
720    relation: ShapeRelation,
721    expression: &DimExpr,
722    input_shapes: &[Option<Vec<usize>>],
723) -> Result<Option<usize>> {
724    evaluate_static_dim_expr_with_inputs(expression, input_shapes).map_err(|cause| {
725        Error::ShapeConstraintEvaluation {
726            family,
727            instruction_index: None,
728            relation,
729            expression: format!("{expression:?}"),
730            cause: cause.into(),
731        }
732    })
733}
734
735fn evaluate_static_dim_expr(expression: &DimExpr) -> Result<Option<usize>> {
736    evaluate_static_dim_expr_without_inputs(expression).map_err(|cause| {
737        Error::ShapeConstraintEvaluation {
738            family: "tenferro-runtime.input.v1",
739            instruction_index: None,
740            relation: ShapeRelation::Equal,
741            expression: format!("{expression:?}"),
742            cause: cause.into(),
743        }
744    })
745}
746
747fn evaluate_static_dim_expr_without_inputs(
748    expression: &DimExpr,
749) -> std::result::Result<Option<usize>, DimExprEvalError> {
750    match expression {
751        DimExpr::Const(value) => Ok(Some(*value)),
752        DimExpr::InputDim { .. } => Ok(None),
753        DimExpr::Add(a, b) => {
754            let Some(lhs) = evaluate_static_dim_expr_without_inputs(a)? else {
755                return Ok(None);
756            };
757            let Some(rhs) = evaluate_static_dim_expr_without_inputs(b)? else {
758                return Ok(None);
759            };
760            lhs.checked_add(rhs)
761                .map(Some)
762                .ok_or(DimExprEvalError::AddOverflow { lhs, rhs })
763        }
764        DimExpr::Sub(a, b) => {
765            let Some(lhs) = evaluate_static_dim_expr_without_inputs(a)? else {
766                return Ok(None);
767            };
768            let Some(rhs) = evaluate_static_dim_expr_without_inputs(b)? else {
769                return Ok(None);
770            };
771            lhs.checked_sub(rhs)
772                .map(Some)
773                .ok_or(DimExprEvalError::SubUnderflow { lhs, rhs })
774        }
775        DimExpr::Mul(a, b) => {
776            let Some(lhs) = evaluate_static_dim_expr_without_inputs(a)? else {
777                return Ok(None);
778            };
779            let Some(rhs) = evaluate_static_dim_expr_without_inputs(b)? else {
780                return Ok(None);
781            };
782            lhs.checked_mul(rhs)
783                .map(Some)
784                .ok_or(DimExprEvalError::MulOverflow { lhs, rhs })
785        }
786        DimExpr::FloorDiv(a, b) => {
787            let Some(lhs) = evaluate_static_dim_expr_without_inputs(a)? else {
788                return Ok(None);
789            };
790            let Some(rhs) = evaluate_static_dim_expr_without_inputs(b)? else {
791                return Ok(None);
792            };
793            if rhs == 0 {
794                return Err(DimExprEvalError::FloorDivByZero { lhs, rhs });
795            }
796            Ok(Some(lhs / rhs))
797        }
798        DimExpr::Min(a, b) => {
799            let Some(lhs) = evaluate_static_dim_expr_without_inputs(a)? else {
800                return Ok(None);
801            };
802            let Some(rhs) = evaluate_static_dim_expr_without_inputs(b)? else {
803                return Ok(None);
804            };
805            Ok(Some(lhs.min(rhs)))
806        }
807        DimExpr::Max(a, b) => {
808            let Some(lhs) = evaluate_static_dim_expr_without_inputs(a)? else {
809                return Ok(None);
810            };
811            let Some(rhs) = evaluate_static_dim_expr_without_inputs(b)? else {
812                return Ok(None);
813            };
814            Ok(Some(lhs.max(rhs)))
815        }
816    }
817}
818
819fn evaluate_static_dim_expr_with_inputs(
820    expression: &DimExpr,
821    input_shapes: &[Option<Vec<usize>>],
822) -> std::result::Result<Option<usize>, DimExprEvalError> {
823    match expression {
824        DimExpr::Const(value) => Ok(Some(*value)),
825        DimExpr::InputDim { input_idx, axis } => match input_shapes.get(*input_idx) {
826            Some(Some(shape)) => {
827                shape
828                    .get(*axis)
829                    .copied()
830                    .map(Some)
831                    .ok_or(DimExprEvalError::AxisOutOfBounds {
832                        input_idx: *input_idx,
833                        axis: *axis,
834                        rank: shape.len(),
835                    })
836            }
837            Some(None) => Ok(None),
838            None => Err(DimExprEvalError::InputOutOfBounds {
839                input_idx: *input_idx,
840                input_count: input_shapes.len(),
841            }),
842        },
843        DimExpr::Add(a, b) => {
844            let Some(lhs) = evaluate_static_dim_expr_with_inputs(a, input_shapes)? else {
845                return Ok(None);
846            };
847            let Some(rhs) = evaluate_static_dim_expr_with_inputs(b, input_shapes)? else {
848                return Ok(None);
849            };
850            lhs.checked_add(rhs)
851                .map(Some)
852                .ok_or(DimExprEvalError::AddOverflow { lhs, rhs })
853        }
854        DimExpr::Sub(a, b) => {
855            let Some(lhs) = evaluate_static_dim_expr_with_inputs(a, input_shapes)? else {
856                return Ok(None);
857            };
858            let Some(rhs) = evaluate_static_dim_expr_with_inputs(b, input_shapes)? else {
859                return Ok(None);
860            };
861            lhs.checked_sub(rhs)
862                .map(Some)
863                .ok_or(DimExprEvalError::SubUnderflow { lhs, rhs })
864        }
865        DimExpr::Mul(a, b) => {
866            let Some(lhs) = evaluate_static_dim_expr_with_inputs(a, input_shapes)? else {
867                return Ok(None);
868            };
869            let Some(rhs) = evaluate_static_dim_expr_with_inputs(b, input_shapes)? else {
870                return Ok(None);
871            };
872            lhs.checked_mul(rhs)
873                .map(Some)
874                .ok_or(DimExprEvalError::MulOverflow { lhs, rhs })
875        }
876        DimExpr::FloorDiv(a, b) => {
877            let Some(lhs) = evaluate_static_dim_expr_with_inputs(a, input_shapes)? else {
878                return Ok(None);
879            };
880            let Some(rhs) = evaluate_static_dim_expr_with_inputs(b, input_shapes)? else {
881                return Ok(None);
882            };
883            if rhs == 0 {
884                return Err(DimExprEvalError::FloorDivByZero { lhs, rhs });
885            }
886            Ok(Some(lhs / rhs))
887        }
888        DimExpr::Min(a, b) => {
889            let Some(lhs) = evaluate_static_dim_expr_with_inputs(a, input_shapes)? else {
890                return Ok(None);
891            };
892            let Some(rhs) = evaluate_static_dim_expr_with_inputs(b, input_shapes)? else {
893                return Ok(None);
894            };
895            Ok(Some(lhs.min(rhs)))
896        }
897        DimExpr::Max(a, b) => {
898            let Some(lhs) = evaluate_static_dim_expr_with_inputs(a, input_shapes)? else {
899                return Ok(None);
900            };
901            let Some(rhs) = evaluate_static_dim_expr_with_inputs(b, input_shapes)? else {
902                return Ok(None);
903            };
904            Ok(Some(lhs.max(rhs)))
905        }
906    }
907}
908
909fn compile_materialized_semantic_program(
910    compiled: &CompiledProgram<StdTensorOp>,
911    descriptors: &[InputDescriptor],
912    scoped_constraints: &[SlotScopedShapeConstraint],
913) -> Result<FrozenProgram> {
914    if compiled.input_slots.len() != descriptors.len() {
915        return Err(Error::runtime_state(
916            "graph_compile_semantic",
917            crate::ErrorPhase::Compile,
918            "materialized input count does not match semantic descriptors",
919        ));
920    }
921
922    let mut builder = SemanticProgramBuilder::new();
923    let mut values = vec![None; compiled.n_slots];
924    let mut slot_shapes = vec![None; compiled.n_slots];
925    let mut guard_slot_shapes = vec![None; compiled.n_slots];
926    let mut slot_dtypes = vec![None; compiled.n_slots];
927    for (input_idx, (&slot, descriptor)) in compiled.input_slots.iter().zip(descriptors).enumerate()
928    {
929        let Some(value_slot) = values.get_mut(slot) else {
930            return Err(invalid_compiled_graph(format!(
931                "semantic input slot {slot} is outside slot table of length {}",
932                compiled.n_slots
933            )));
934        };
935        let semantic_shape = descriptor.semantic_shape(input_idx);
936        let value = builder
937            .input(ProgramInputSpec::new(
938                descriptor.dtype,
939                semantic_shape.clone(),
940            ))
941            .map_err(semantic_build_error)?;
942        if let Some(tensor) = &descriptor.default_tensor {
943            builder
944                .bind_input(value, Arc::clone(tensor))
945                .map_err(semantic_build_error)?;
946        }
947        *value_slot = Some(value);
948        slot_shapes[slot] = Some(semantic_shape);
949        guard_slot_shapes[slot] = Some(descriptor.constraint_guard_shape(input_idx));
950        slot_dtypes[slot] = Some(descriptor.dtype);
951    }
952
953    for instruction in &compiled.instructions {
954        let inputs = instruction
955            .inputs
956            .iter()
957            .map(|&slot| {
958                values.get(slot).and_then(|value| *value).ok_or_else(|| {
959                    invalid_compiled_graph(format!(
960                        "semantic operation input slot {slot} is unavailable"
961                    ))
962                })
963            })
964            .collect::<Result<Vec<_>>>()?;
965        let guard_input_shapes = instruction
966            .inputs
967            .iter()
968            .map(|&slot| {
969                guard_slot_shapes
970                    .get(slot)
971                    .and_then(|shape| shape.as_deref())
972                    .ok_or_else(|| {
973                        invalid_compiled_graph(format!(
974                            "semantic operation guard-shape input slot {slot} is unavailable"
975                        ))
976                    })
977            })
978            .collect::<Result<Vec<_>>>()?;
979        let guard_output_shapes = match &instruction.operation {
980            StdTensorOp::Extension(extension) => {
981                let input_dtypes = instruction
982                    .inputs
983                    .iter()
984                    .map(|&slot| {
985                        slot_dtypes
986                            .get(slot)
987                            .and_then(|dtype| *dtype)
988                            .ok_or_else(|| {
989                                invalid_compiled_graph(format!(
990                                    "semantic operation dtype input slot {slot} is unavailable"
991                                ))
992                            })
993                    })
994                    .collect::<Result<Vec<_>>>()?;
995                infer_extension_output_meta(extension.as_ref(), &input_dtypes, &guard_input_shapes)?
996                    .into_iter()
997                    .map(|(_dtype, shape)| shape)
998                    .collect::<Vec<_>>()
999            }
1000            operation => infer_output_shapes(operation, &guard_input_shapes)?,
1001        };
1002        let outputs = match &instruction.operation {
1003            StdTensorOp::Extension(extension) => builder
1004                .add_extension(Arc::clone(extension), &inputs)
1005                .map_err(semantic_build_error)?,
1006            operation => builder
1007                .add_op(
1008                    CoreSemanticOp::try_from(operation).map_err(|source| {
1009                        Error::runtime_state_source(
1010                            "graph_compile_semantic",
1011                            crate::ErrorPhase::Compile,
1012                            source,
1013                        )
1014                    })?,
1015                    &inputs,
1016                )
1017                .map_err(semantic_build_error)?,
1018        };
1019        if outputs.len() != instruction.outputs.len() {
1020            return Err(invalid_compiled_graph(format!(
1021                "semantic operation produced {} outputs for {} materialized slots",
1022                outputs.len(),
1023                instruction.outputs.len()
1024            )));
1025        }
1026        if guard_output_shapes.len() != instruction.outputs.len() {
1027            return Err(invalid_compiled_graph(format!(
1028                "semantic operation inferred {} guard shapes for {} materialized slots",
1029                guard_output_shapes.len(),
1030                instruction.outputs.len()
1031            )));
1032        }
1033        for ((&slot, &value), guard_shape) in instruction
1034            .outputs
1035            .iter()
1036            .zip(outputs.iter())
1037            .zip(guard_output_shapes.iter())
1038        {
1039            let Some(value_slot) = values.get_mut(slot) else {
1040                return Err(invalid_compiled_graph(format!(
1041                    "semantic output slot {slot} is outside slot table of length {}",
1042                    compiled.n_slots
1043                )));
1044            };
1045            if value_slot.replace(value).is_some() {
1046                return Err(invalid_compiled_graph(format!(
1047                    "semantic output slot {slot} has multiple producers"
1048                )));
1049            }
1050            let metadata = builder
1051                .value_metadata(value)
1052                .map_err(semantic_build_error)?;
1053            slot_shapes[slot] = Some(
1054                metadata
1055                    .shape()
1056                    .iter()
1057                    .enumerate()
1058                    .map(|(axis, extent)| match extent {
1059                        tenferro_ops::ShapeExtent::Exact(expression)
1060                        | tenferro_ops::ShapeExtent::UpperBound(expression) => expression.clone(),
1061                        tenferro_ops::ShapeExtent::Unknown => DimExpr::InputDim {
1062                            input_idx: slot,
1063                            axis,
1064                        },
1065                    })
1066                    .collect(),
1067            );
1068            guard_slot_shapes[slot] = Some(guard_shape.clone());
1069            slot_dtypes[slot] = Some(metadata.dtype());
1070        }
1071    }
1072
1073    for scoped in scoped_constraints {
1074        let target = scoped
1075            .origin_slots
1076            .iter()
1077            .find_map(|&slot| values.get(slot).and_then(|value| *value))
1078            .ok_or_else(|| {
1079                invalid_compiled_graph(
1080                    "semantic shape constraint has no available origin".to_string(),
1081                )
1082            })?;
1083        let lhs = lower_scoped_dim_expr(
1084            &scoped.local.lhs,
1085            &scoped.input_slots,
1086            &guard_slot_shapes,
1087            &scoped.local,
1088        )?;
1089        let rhs = lower_scoped_dim_expr(
1090            &scoped.local.rhs,
1091            &scoped.input_slots,
1092            &guard_slot_shapes,
1093            &scoped.local,
1094        )?;
1095        let relation = match scoped.local.relation {
1096            tenferro_ops::ShapeRelation::Equal => ProgramShapeRelation::Equal,
1097        };
1098        let lowered = LocalShapeConstraint {
1099            source: scoped.local.source.clone(),
1100            relation: scoped.local.relation,
1101            lhs,
1102            rhs,
1103        };
1104        let retained_guards = discharge(vec![lowered])?;
1105        if retained_guards.is_empty() {
1106            continue;
1107        }
1108        let guards = retained_guards.into_iter().map(|guard| {
1109            ProgramShapeGuard::new(relation, guard.lhs, guard.rhs)
1110                .with_source_family(guard.source.family_id)
1111        });
1112        builder
1113            .add_shape_guards_to_output(target, guards)
1114            .map_err(semantic_build_error)?;
1115    }
1116
1117    let outputs = compiled
1118        .output_slots
1119        .iter()
1120        .map(|&slot| {
1121            values.get(slot).and_then(|value| *value).ok_or_else(|| {
1122                invalid_compiled_graph(format!(
1123                    "semantic program output slot {slot} is unavailable"
1124                ))
1125            })
1126        })
1127        .collect::<Result<Vec<_>>>()?;
1128    builder.finish(&outputs).map_err(|source| {
1129        Error::runtime_state_source("graph_compile_semantic", crate::ErrorPhase::Compile, source)
1130    })
1131}
1132
1133fn semantic_build_error(source: crate::program::ProgramBuildError) -> Error {
1134    Error::runtime_state_source("graph_compile_semantic", crate::ErrorPhase::Compile, source)
1135}
1136
1137impl Default for GraphCompiler {
1138    fn default() -> Self {
1139        Self::new()
1140    }
1141}
1142
1143fn collect_default_inputs(
1144    outputs: &[&TracedTensor],
1145) -> Result<HashMap<TensorInputKey, Arc<Tensor>>> {
1146    let mut all_inputs = HashMap::new();
1147    for output in outputs {
1148        for (key, tensor) in output.inputs_map.iter() {
1149            if let Some(existing) = all_inputs.get(key) {
1150                if !default_tensors_equivalent(existing, tensor) {
1151                    return Err(Error::DuplicateBinding {
1152                        input_key: format!("{:?}", key),
1153                    });
1154                }
1155                continue;
1156            }
1157            all_inputs.insert(key.clone(), tensor.clone());
1158        }
1159    }
1160    Ok(all_inputs)
1161}
1162
1163fn insert_checkpoint_alias(
1164    aliases: &mut HashMap<TensorInputKey, ValueKey<StdTensorOp>>,
1165    alias_key: TensorInputKey,
1166    target_key: ValueKey<StdTensorOp>,
1167) -> Result<()> {
1168    if let Some(existing) = aliases.get(&alias_key) {
1169        if existing != &target_key {
1170            return Err(Error::Internal(format!(
1171                "checkpoint alias {alias_key:?} targets both {existing:?} and {target_key:?}"
1172            )));
1173        }
1174        return Ok(());
1175    }
1176    aliases.insert(alias_key, target_key);
1177    Ok(())
1178}
1179
1180struct AliasAwareMaterializer<'a> {
1181    view: &'a ResolvedView<StdTensorOp>,
1182    aliases: &'a HashMap<TensorInputKey, ValueKey<StdTensorOp>>,
1183    alias_shapes: &'a HashMap<TensorInputKey, Vec<usize>>,
1184    val_map: HashMap<ValueKey<StdTensorOp>, usize>,
1185    op_map: HashMap<Arc<OperationKey<StdTensorOp>>, usize>,
1186    values: Vec<MaterializedValue<StdTensorOp>>,
1187    operations: Vec<MaterializedOperation<StdTensorOp>>,
1188    input_keys: Vec<ValueKey<StdTensorOp>>,
1189}
1190
1191impl<'a> AliasAwareMaterializer<'a> {
1192    fn new(
1193        view: &'a ResolvedView<StdTensorOp>,
1194        aliases: &'a HashMap<TensorInputKey, ValueKey<StdTensorOp>>,
1195        alias_shapes: &'a HashMap<TensorInputKey, Vec<usize>>,
1196    ) -> Self {
1197        Self {
1198            view,
1199            aliases,
1200            alias_shapes,
1201            val_map: HashMap::new(),
1202            op_map: HashMap::new(),
1203            values: Vec::new(),
1204            operations: Vec::new(),
1205            input_keys: Vec::new(),
1206        }
1207    }
1208
1209    fn visit(&mut self, key: &ValueKey<StdTensorOp>) -> usize {
1210        if let Some(&index) = self.val_map.get(key) {
1211            return index;
1212        }
1213        if let Some(target) = self.alias_target(key).cloned() {
1214            let index = self.visit(&target);
1215            let index = self.refine_alias_to_checkpoint_shape(key, index);
1216            self.val_map.insert(key.clone(), index);
1217            return index;
1218        }
1219
1220        let resolved = self.view.resolve_value(key);
1221        assert!(
1222            resolved.is_some(),
1223            "key not found in resolved view: {:?}",
1224            key
1225        );
1226        match resolved {
1227            Some(ValueDef::Input { .. }) => self.materialize_input(key),
1228            Some(ValueDef::Produced {
1229                operation,
1230                input_keys,
1231                role,
1232                output_slot,
1233            }) => self.materialize_produced(operation, input_keys, role, output_slot),
1234            None => unreachable!("asserted above"),
1235        }
1236    }
1237
1238    fn alias_target(&self, key: &ValueKey<StdTensorOp>) -> Option<&ValueKey<StdTensorOp>> {
1239        let ValueKey::Input(input_key) = key else {
1240            return None;
1241        };
1242        self.aliases.get(input_key)
1243    }
1244
1245    fn refine_alias_to_checkpoint_shape(
1246        &mut self,
1247        key: &ValueKey<StdTensorOp>,
1248        target_index: usize,
1249    ) -> usize {
1250        let ValueKey::Input(input_key) = key else {
1251            return target_index;
1252        };
1253        let Some(shape) = self.alias_shapes.get(input_key) else {
1254            return target_index;
1255        };
1256        let target_key = self.values[target_index].key.clone();
1257        let rank = shape.len();
1258        self.materialize_produced(
1259            StdTensorOp::Slice(SliceConfig {
1260                starts: vec![0; rank],
1261                limits: shape.clone(),
1262                strides: vec![1; rank],
1263            }),
1264            vec![target_key],
1265            computegraph::types::OperationRole::Primary,
1266            0,
1267        )
1268    }
1269
1270    fn materialize_input(&mut self, key: &ValueKey<StdTensorOp>) -> usize {
1271        let index = self.values.len();
1272        self.values.push(MaterializedValue {
1273            key: key.clone(),
1274            producer: None,
1275        });
1276        self.val_map.insert(key.clone(), index);
1277        self.input_keys.push(key.clone());
1278        index
1279    }
1280
1281    fn materialize_produced(
1282        &mut self,
1283        operation: StdTensorOp,
1284        input_keys: Vec<ValueKey<StdTensorOp>>,
1285        role: computegraph::types::OperationRole,
1286        output_slot: usize,
1287    ) -> usize {
1288        let op_key = Arc::new(OperationKey::new(
1289            operation.clone(),
1290            input_keys.clone(),
1291            role.clone(),
1292        ));
1293
1294        if self.op_map.contains_key(&op_key) {
1295            let output_key = ValueKey::Derived {
1296                operation: op_key,
1297                output_slot: output_slot as u8,
1298            };
1299            let val_index = self.val_map.get(&output_key).copied();
1300            assert!(
1301                val_index.is_some(),
1302                "materialized op {:?} is missing output slot {}",
1303                operation,
1304                output_slot
1305            );
1306            return match val_index {
1307                Some(index) => index,
1308                None => unreachable!("asserted above"),
1309            };
1310        }
1311
1312        let materialized_inputs = input_keys.iter().map(|input| self.visit(input)).collect();
1313        let op_index = self.operations.len();
1314        self.op_map.insert(Arc::clone(&op_key), op_index);
1315        self.operations.push(MaterializedOperation {
1316            operation: operation.clone(),
1317            inputs: materialized_inputs,
1318            outputs: Vec::with_capacity(operation.output_count()),
1319            role,
1320        });
1321
1322        for slot in 0..operation.output_count() {
1323            let output_key = ValueKey::Derived {
1324                operation: Arc::clone(&op_key),
1325                output_slot: slot as u8,
1326            };
1327            let val_index = self.values.len();
1328            self.values.push(MaterializedValue {
1329                key: output_key.clone(),
1330                producer: Some((op_index, slot)),
1331            });
1332            self.val_map.insert(output_key, val_index);
1333            self.operations[op_index].outputs.push(val_index);
1334        }
1335
1336        self.operations[op_index].outputs[output_slot]
1337    }
1338}
1339
1340fn materialize_merge_with_input_aliases(
1341    view: &ResolvedView<StdTensorOp>,
1342    outputs: &[ValueKey<StdTensorOp>],
1343    aliases: &HashMap<TensorInputKey, ValueKey<StdTensorOp>>,
1344    alias_shapes: &HashMap<TensorInputKey, Vec<usize>>,
1345) -> MaterializedGraph<StdTensorOp> {
1346    let mut materializer = AliasAwareMaterializer::new(view, aliases, alias_shapes);
1347    let mut materialized_outputs = Vec::with_capacity(outputs.len());
1348
1349    for output in outputs {
1350        let output_slot = materializer.visit(output);
1351        materialized_outputs.push(materializer.values[output_slot].key.clone());
1352    }
1353
1354    MaterializedGraph {
1355        values: materializer.values,
1356        operations: materializer.operations,
1357        inputs: materializer.input_keys,
1358        outputs: materialized_outputs,
1359    }
1360}
1361
1362fn validate_placeholder_spec(
1363    index: usize,
1364    placeholder: &TracedTensor,
1365    dtype: DType,
1366    shape: &[usize],
1367) -> Result<()> {
1368    if placeholder.data.is_some() {
1369        return Err(Error::UnexpectedBinding {
1370            binding_index: index,
1371        });
1372    }
1373    placeholder.input_key().ok_or(Error::UnexpectedBinding {
1374        binding_index: index,
1375    })?;
1376
1377    if placeholder.dtype != dtype {
1378        return Err(Error::PlaceholderDtypeMismatch {
1379            expected: placeholder.dtype,
1380            actual: dtype,
1381        });
1382    }
1383    validate_placeholder_shape(placeholder, shape)
1384}
1385
1386fn validate_placeholder_shape(placeholder: &TracedTensor, shape: &[usize]) -> Result<()> {
1387    match try_concrete_shape(placeholder) {
1388        Some(expected_shape) => {
1389            if expected_shape.as_slice() != shape {
1390                return Err(Error::PlaceholderShapeMismatch {
1391                    expected: expected_shape,
1392                    actual: shape.to_vec(),
1393                });
1394            }
1395        }
1396        None => {
1397            if placeholder.rank != shape.len() {
1398                return Err(Error::PlaceholderRankMismatch {
1399                    expected: placeholder.rank,
1400                    actual: shape.len(),
1401                });
1402            }
1403        }
1404    }
1405    Ok(())
1406}
1407
1408fn descriptor_for_input(
1409    key: &TensorInputKey,
1410    binding_specs: &HashMap<TensorInputKey, InputDescriptor>,
1411    default_inputs: &HashMap<TensorInputKey, Arc<Tensor>>,
1412    allow_unbound_placeholders: bool,
1413) -> Result<InputDescriptor> {
1414    if let Some(tensor) = default_inputs.get(key) {
1415        return Ok(InputDescriptor {
1416            dtype: tensor.dtype(),
1417            shape: tensor.shape().to_vec(),
1418            extent_identity: default_input_extent_identity(key, tensor)?,
1419            default_tensor: Some(tensor.clone()),
1420        });
1421    }
1422    if let Some(spec) = binding_specs.get(key) {
1423        return Ok(spec.clone());
1424    }
1425    if allow_unbound_placeholders {
1426        return descriptor_for_unbound_input(key);
1427    }
1428    Err(Error::UnboundPlaceholder {
1429        input_key: format!("{:?}", key),
1430    })
1431}
1432
1433fn descriptor_for_unbound_input(key: &TensorInputKey) -> Result<InputDescriptor> {
1434    let metadata = registered_meta(&ValueKey::Input(key.clone()))?;
1435    if let Some(shape) = metadata
1436        .exact_shape()
1437        .as_deref()
1438        .and_then(concrete_shape_from_sym_dims)
1439    {
1440        return Ok(InputDescriptor {
1441            dtype: metadata.dtype,
1442            shape,
1443            extent_identity: InputExtentIdentity::Concrete,
1444            default_tensor: None,
1445        });
1446    }
1447    Ok(InputDescriptor {
1448        dtype: metadata.dtype,
1449        shape: vec![0; metadata.rank()],
1450        extent_identity: InputExtentIdentity::Symbolic,
1451        default_tensor: None,
1452    })
1453}
1454
1455fn concrete_shape_from_sym_dims(shape: &[SymDim]) -> Option<Vec<usize>> {
1456    shape.iter().map(SymDim::constant_value).collect()
1457}
1458
1459fn default_input_extent_identity(
1460    key: &TensorInputKey,
1461    tensor: &Tensor,
1462) -> Result<InputExtentIdentity> {
1463    let metadata = registered_meta(&ValueKey::Input(key.clone()))?;
1464    let exact_shape = metadata.exact_shape();
1465    if metadata.dtype == tensor.dtype()
1466        && exact_shape_matches_tensor_shape(exact_shape.as_deref(), tensor.shape())
1467    {
1468        Ok(InputExtentIdentity::Concrete)
1469    } else {
1470        Ok(InputExtentIdentity::Symbolic)
1471    }
1472}
1473
1474fn exact_shape_matches_tensor_shape(
1475    shape: Option<&[tenferro_ops::SymDim]>,
1476    tensor_shape: &[usize],
1477) -> bool {
1478    let Some(shape) = shape else {
1479        return false;
1480    };
1481    shape.len() == tensor_shape.len()
1482        && shape
1483            .iter()
1484            .zip(tensor_shape)
1485            .all(|(dim, &extent)| dim.constant_value() == Some(extent))
1486}
1487
1488fn prune_compiled_extension_outputs(prog: &mut CompiledProgram<StdTensorOp>) -> Result<()> {
1489    let mut live_slots = vec![false; prog.n_slots];
1490    for &slot in &prog.output_slots {
1491        let Some(live) = live_slots.get_mut(slot) else {
1492            return Err(invalid_compiled_graph(format!(
1493                "program output slot {slot} is outside slot table of length {}",
1494                prog.n_slots
1495            )));
1496        };
1497        *live = true;
1498    }
1499
1500    for instr in prog.instructions.iter_mut().rev() {
1501        let live_outputs = instr
1502            .outputs
1503            .iter()
1504            .map(|&slot| {
1505                live_slots.get(slot).copied().ok_or_else(|| {
1506                    invalid_compiled_graph(format!(
1507                        "instruction output slot {slot} is outside slot table of length {}",
1508                        prog.n_slots
1509                    ))
1510                })
1511            })
1512            .collect::<Result<Vec<_>>>()?;
1513
1514        if let StdTensorOp::Extension(ext) = &instr.operation {
1515            if let Some(pruned) = ext.prune_outputs(&live_outputs) {
1516                let kept_outputs = instr
1517                    .outputs
1518                    .iter()
1519                    .zip(live_outputs.iter())
1520                    .filter_map(|(&slot, &live)| live.then_some(slot))
1521                    .collect::<Vec<_>>();
1522                if pruned.output_count() != kept_outputs.len() {
1523                    return Err(invalid_compiled_graph(format!(
1524                        "extension family_id={:?} pruned to {} outputs for {} live slots",
1525                        ext.family_id(),
1526                        pruned.output_count(),
1527                        kept_outputs.len()
1528                    )));
1529                }
1530                instr.operation = StdTensorOp::Extension(pruned);
1531                instr.outputs = kept_outputs;
1532            }
1533        }
1534
1535        if live_outputs.iter().any(|&live| live) {
1536            for &slot in &instr.inputs {
1537                let Some(live) = live_slots.get_mut(slot) else {
1538                    return Err(invalid_compiled_graph(format!(
1539                        "instruction input slot {slot} is outside slot table of length {}",
1540                        prog.n_slots
1541                    )));
1542                };
1543                *live = true;
1544            }
1545        }
1546    }
1547
1548    Ok(())
1549}
1550
1551fn invalid_compiled_graph(message: impl Into<String>) -> Error {
1552    Error::Internal(message.into())
1553}
1554
1555fn default_tensors_equivalent(lhs: &Arc<Tensor>, rhs: &Arc<Tensor>) -> bool {
1556    if Arc::ptr_eq(lhs, rhs) {
1557        return true;
1558    }
1559    if lhs.dtype() != rhs.dtype() || lhs.shape() != rhs.shape() {
1560        return false;
1561    }
1562    match lhs.dtype() {
1563        DType::F32 => default_slices_equivalent::<f32>(lhs, rhs),
1564        DType::F64 => default_slices_equivalent::<f64>(lhs, rhs),
1565        DType::I32 => default_slices_equivalent::<i32>(lhs, rhs),
1566        DType::I64 => default_slices_equivalent::<i64>(lhs, rhs),
1567        DType::Bool => default_slices_equivalent::<bool>(lhs, rhs),
1568        DType::C32 => default_slices_equivalent::<Complex32>(lhs, rhs),
1569        DType::C64 => default_slices_equivalent::<Complex64>(lhs, rhs),
1570    }
1571}
1572
1573fn default_slices_equivalent<T: TensorScalar + PartialEq>(lhs: &Tensor, rhs: &Tensor) -> bool {
1574    match (lhs.as_slice::<T>(), rhs.as_slice::<T>()) {
1575        (Ok(lhs), Ok(rhs)) => lhs == rhs,
1576        // Backend-resident defaults cannot be inspected here; only the same
1577        // Arc<Tensor> is considered equivalent by `default_tensors_equivalent`.
1578        _ => false,
1579    }
1580}
1581
1582#[cfg(test)]
1583mod constraint_scope_tests;
1584
1585#[cfg(test)]
1586mod test_support;
1587
1588#[cfg(test)]
1589mod tests {
1590    use super::*;
1591    use std::any::Any;
1592    use std::hash::Hasher;
1593    use std::sync::Arc;
1594    use tenferro_ops::{
1595        ext_op::{ExtensionAliasDeclaration, ExtensionEffectDeclaration, ExtensionOp},
1596        SymDim,
1597    };
1598    use tenferro_tensor::{
1599        Buffer, BufferHandle, DeviceId, DeviceKind, GpuBackendKind, MemoryKind, Placement,
1600        TypedTensor,
1601    };
1602
1603    #[test]
1604    fn compile_publishes_semantic_program_and_separate_default_bindings() {
1605        let input = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1606        let output = input.neg().unwrap();
1607
1608        let program = GraphCompiler::new().compile(&output).unwrap();
1609
1610        assert_eq!(program.program().inputs().len(), 1);
1611        assert_eq!(program.program().outputs().len(), 1);
1612        assert_eq!(program.program().operations().count(), 1);
1613        assert_eq!(program.bindings().len(), 1);
1614        assert_eq!(
1615            program
1616                .program()
1617                .value_metadata(program.program().inputs()[0])
1618                .unwrap()
1619                .shape(),
1620            &[ShapeExtent::Exact(DimExpr::Const(2))]
1621        );
1622        assert_eq!(
1623            program
1624                .program()
1625                .value_metadata(program.program().outputs()[0])
1626                .unwrap()
1627                .dtype(),
1628            DType::F64
1629        );
1630    }
1631
1632    #[test]
1633    fn compile_preserves_symbolic_default_input_extent_identity() {
1634        let tensor = Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1635        let input = TracedTensor::from_tensor_symbolic_shape(tensor).unwrap();
1636        let output = input.neg().unwrap();
1637
1638        let program = GraphCompiler::new().compile(&output).unwrap();
1639
1640        assert!(matches!(
1641            program
1642                .program()
1643                .value_metadata(program.program().inputs()[0])
1644                .unwrap()
1645                .shape(),
1646            [ShapeExtent::Exact(DimExpr::InputDim {
1647                input_idx: 0,
1648                axis: 0
1649            })]
1650        ));
1651    }
1652
1653    #[test]
1654    fn compile_many_rejects_conflicting_default_inputs_for_same_key() {
1655        let x = TracedTensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap();
1656        let y1 = x.neg().unwrap();
1657        let mut y2 = x.neg().unwrap();
1658        let key = x.input_key().expect("concrete traced tensor has input key");
1659        let replacement = Arc::new(Tensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap());
1660        let mut inputs = (*y2.inputs_map).clone();
1661        inputs.insert(key.clone(), replacement);
1662        y2.inputs_map = Arc::new(inputs);
1663
1664        let err = GraphCompiler::new().compile_many(&[&y1, &y2]).unwrap_err();
1665
1666        assert!(matches!(
1667            err,
1668            Error::DuplicateBinding { ref input_key } if input_key.contains(&format!("{key:?}"))
1669        ));
1670    }
1671
1672    #[test]
1673    fn default_tensors_equivalent_rejects_distinct_backend_buffers() {
1674        let placement = Placement {
1675            memory_kind: MemoryKind::Device,
1676            device: Some(DeviceId {
1677                kind: DeviceKind::Gpu(GpuBackendKind::Cuda),
1678                ordinal: 0,
1679            }),
1680            cpu_affinity: None,
1681        };
1682        let lhs = Arc::new(Tensor::F64(
1683            TypedTensor::from_buffer_col_major(
1684                vec![2],
1685                Buffer::Backend(Arc::new(BufferHandle::<f64>::new_with_len(1, 2))),
1686                placement.clone(),
1687            )
1688            .unwrap(),
1689        ));
1690        let rhs = Arc::new(Tensor::F64(
1691            TypedTensor::from_buffer_col_major(
1692                vec![2],
1693                Buffer::Backend(Arc::new(BufferHandle::<f64>::new_with_len(2, 2))),
1694                placement,
1695            )
1696            .unwrap(),
1697        ));
1698
1699        assert!(
1700            !default_tensors_equivalent(&lhs, &rhs),
1701            "distinct backend-resident default tensors must not compare equal just because both are unreadable on host"
1702        );
1703        assert!(default_tensors_equivalent(&lhs, &lhs));
1704    }
1705
1706    #[test]
1707    fn compile_frozen_program_rejects_static_unbound_shape_guard_mismatch() {
1708        let mut builder = SemanticProgramBuilder::new();
1709        let lhs = builder
1710            .input(ProgramInputSpec::new(DType::F64, [DimExpr::Const(2)]))
1711            .unwrap();
1712        let rhs = builder
1713            .input(ProgramInputSpec::new(DType::F64, [DimExpr::Const(3)]))
1714            .unwrap();
1715        let output = builder.add_op(CoreSemanticOp::Neg, &[lhs]).unwrap()[0];
1716        builder
1717            .add_shape_guards_to_output(
1718                output,
1719                [ProgramShapeGuard::new(
1720                    ProgramShapeRelation::Equal,
1721                    DimExpr::InputDim {
1722                        input_idx: 0,
1723                        axis: 0,
1724                    },
1725                    DimExpr::InputDim {
1726                        input_idx: 1,
1727                        axis: 0,
1728                    },
1729                )],
1730            )
1731            .unwrap();
1732        let frozen = builder.finish(&[output]).unwrap();
1733
1734        let err = GraphCompiler::new()
1735            .compile_frozen_program(&frozen)
1736            .unwrap_err();
1737
1738        assert!(matches!(
1739            err,
1740            Error::ShapeConstraintViolation {
1741                lhs_value: 2,
1742                rhs_value: 3,
1743                ..
1744            }
1745        ));
1746        let _ = rhs;
1747    }
1748
1749    #[test]
1750    fn compile_frozen_program_defers_dynamic_unbound_shape_guard() {
1751        let mut builder = SemanticProgramBuilder::new();
1752        let lhs = builder
1753            .input(ProgramInputSpec::new(DType::F64, [DimExpr::Const(2)]))
1754            .unwrap();
1755        let rhs = builder
1756            .input(ProgramInputSpec::from_metadata(
1757                ProgramValueMetadata::from_extents(DType::F64, [ShapeExtent::Unknown]),
1758            ))
1759            .unwrap();
1760        let output = builder.add_op(CoreSemanticOp::Neg, &[lhs]).unwrap()[0];
1761        builder
1762            .add_shape_guards_to_output(
1763                output,
1764                [ProgramShapeGuard::new(
1765                    ProgramShapeRelation::Equal,
1766                    DimExpr::InputDim {
1767                        input_idx: 0,
1768                        axis: 0,
1769                    },
1770                    DimExpr::InputDim {
1771                        input_idx: 1,
1772                        axis: 0,
1773                    },
1774                )],
1775            )
1776            .unwrap();
1777        let frozen = builder.finish(&[output]).unwrap();
1778
1779        GraphCompiler::new()
1780            .compile_frozen_program(&frozen)
1781            .unwrap();
1782        let _ = rhs;
1783    }
1784
1785    #[derive(Clone, Debug, PartialEq, Eq)]
1786    struct PrunableTestOp {
1787        pruned: bool,
1788    }
1789
1790    impl ExtensionOp for PrunableTestOp {
1791        fn family_id(&self) -> &'static str {
1792            "tenferro-runtime.test-prunable.v1"
1793        }
1794
1795        fn payload_hash(&self, hasher: &mut dyn Hasher) {
1796            hasher.write_u8(u8::from(self.pruned));
1797        }
1798
1799        fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
1800            other
1801                .as_any()
1802                .downcast_ref::<Self>()
1803                .is_some_and(|that| self == that)
1804        }
1805
1806        fn clone_arc(&self) -> Arc<dyn ExtensionOp> {
1807            Arc::new(self.clone())
1808        }
1809
1810        fn as_any(&self) -> &dyn Any {
1811            self
1812        }
1813
1814        fn input_count(&self) -> usize {
1815            1
1816        }
1817
1818        fn output_count(&self) -> usize {
1819            if self.pruned {
1820                1
1821            } else {
1822                3
1823            }
1824        }
1825
1826        fn semantic_effects(&self) -> ExtensionEffectDeclaration<'_> {
1827            ExtensionEffectDeclaration::Declared(&[])
1828        }
1829
1830        fn semantic_aliases(&self) -> ExtensionAliasDeclaration<'_> {
1831            ExtensionAliasDeclaration::AllFresh
1832        }
1833
1834        fn infer_output_meta(
1835            &self,
1836            ctx: &mut tenferro_ops::ExtensionShapeContext<'_>,
1837        ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1838            let dtype = ctx.input_dtype(0)?;
1839            let shape = ctx.input_shape(0)?.to_vec();
1840            Ok((0..self.output_count())
1841                .map(|_| (dtype, shape.clone()))
1842                .collect())
1843        }
1844
1845        fn prune_outputs(&self, live_outputs: &[bool]) -> Option<Arc<dyn ExtensionOp>> {
1846            (!self.pruned && live_outputs == [false, true, false])
1847                .then(|| Arc::new(Self { pruned: true }) as Arc<dyn ExtensionOp>)
1848        }
1849    }
1850
1851    #[test]
1852    fn compile_prunes_extension_outputs_with_replacement_op() {
1853        let input = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1854        let outputs =
1855            crate::extension::apply(Arc::new(PrunableTestOp { pruned: false }), &[&input]).unwrap();
1856
1857        let program = GraphCompiler::new().compile(&outputs[1]).unwrap();
1858        let staging =
1859            stage_semantic_program(program.program(), program.compiler_options()).unwrap();
1860        let pruned_instruction = staging
1861            .instructions
1862            .iter()
1863            .find_map(|inst| match &inst.op {
1864                crate::exec::ExecOp::Extension(op)
1865                    if op.family_id() == "tenferro-runtime.test-prunable.v1" =>
1866                {
1867                    Some((
1868                        inst.output_slots.clone(),
1869                        format!("{op:?}"),
1870                        op.output_count(),
1871                    ))
1872                }
1873                _ => None,
1874            })
1875            .expect("compiled program should contain the test extension");
1876
1877        assert_eq!(pruned_instruction.0.len(), 1);
1878        assert!(pruned_instruction.1.contains("pruned: true"));
1879        assert_eq!(pruned_instruction.2, 1);
1880    }
1881
1882    #[test]
1883    fn compiled_graph_input_keys_preserve_order_for_binary_graph() {
1884        let a = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1885        let b = TracedTensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap();
1886        let a_key = a.input_key().expect("concrete traced tensor has input key");
1887        let b_key = b.input_key().expect("concrete traced tensor has input key");
1888        let c = a.mul(&b).unwrap();
1889
1890        let program = GraphCompiler::new().compile_many(&[&c]).unwrap();
1891
1892        assert_eq!(program.input_count(), 2);
1893        assert_eq!(program.input_keys().len(), 2);
1894        let keys: Vec<_> = program.input_keys().to_vec();
1895        assert!(keys.contains(&a_key), "input_keys must contain a's key");
1896        assert!(keys.contains(&b_key), "input_keys must contain b's key");
1897        // input_key_index maps each key to its position
1898        assert_eq!(
1899            program.input_key_index(&a_key),
1900            keys.iter().position(|k| k == &a_key)
1901        );
1902        assert_eq!(
1903            program.input_key_index(&b_key),
1904            keys.iter().position(|k| k == &b_key)
1905        );
1906    }
1907
1908    #[test]
1909    fn compile_with_input_specs_reorders_inputs_by_explicit_order() {
1910        let a = TracedTensor::input_symbolic_shape(DType::F64, 1).unwrap();
1911        let b = TracedTensor::input_symbolic_shape(DType::F64, 1).unwrap();
1912        let a_key = a.input_key().expect("symbolic traced tensor has input key");
1913        let b_key = b.input_key().expect("symbolic traced tensor has input key");
1914        let c = a.mul(&b).unwrap();
1915
1916        // Request b first, then a
1917        let program = GraphCompiler::new()
1918            .compile_with_input_specs(&c, &[(&b, DType::F64, &[2]), (&a, DType::F64, &[2])])
1919            .unwrap();
1920
1921        assert_eq!(program.input_count(), 2);
1922        assert_eq!(program.input_keys().len(), 2);
1923        // b must be at position 0 per the explicit order
1924        assert_eq!(
1925            program.input_key_index(&b_key),
1926            Some(0),
1927            "b must be first in explicit order"
1928        );
1929        assert_eq!(
1930            program.input_key_index(&a_key),
1931            Some(1),
1932            "a must be second in explicit order"
1933        );
1934    }
1935}