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