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