Skip to main content

tenferro_runtime/
traced.rs

1use std::collections::HashMap;
2use std::error::Error as StdError;
3use std::fmt;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::Arc;
6
7use computegraph::graph::{Graph, GraphBuilder};
8use computegraph::types::{OperationRole, ValueKey, ValueRef};
9use computegraph::LocalValueId;
10use num_complex::{Complex32, Complex64};
11use tenferro_ops::ad::context::GlobalMetadataScope;
12use tenferro_ops::broadcast::{
13    broadcast_error_to_validation, broadcast_in_dim_extent_error, broadcast_input_plan,
14    broadcast_shape, broadcast_shapes, BroadcastError,
15};
16use tenferro_ops::dim_expr::DimExpr;
17use tenferro_ops::input_key::TensorInputKey;
18use tenferro_ops::std_tensor_op::StdTensorOp;
19use tenferro_ops::TensorMeta;
20use tenferro_tensor::{
21    CompareDir, DType, DotGeneralConfig, Error as TensorError, GatherConfig, IntoShapeVec,
22    PadConfig, ScatterConfig, ShapeMismatch, SliceConfig, Tensor, TensorScalar, TensorValue,
23    ValidationError,
24};
25
26use super::error::{Error, ErrorPhase, Result};
27use super::sym_dim::SymDim;
28use crate::checkpoint::{CheckpointNode, RetainedInputMap, RetainedValue};
29use crate::metadata::{
30    concrete_tensor_meta, register_scoped_graph_metadata, register_scoped_value_metadata,
31    symbolic_input_meta, tensor_meta, MetadataScopeChain,
32};
33use crate::scalar_semantics::{bool_from_real_for_op, round_real_to_i32_for_op, round_real_to_i64};
34use crate::shape_constraint::ConstraintScopeChain;
35
36static NEXT_INPUT_ID: AtomicU64 = AtomicU64::new(0);
37static NEXT_TRACED_ID: AtomicU64 = AtomicU64::new(0);
38
39pub type TracedTensorId = u64;
40
41pub(crate) fn next_input_key() -> TensorInputKey {
42    TensorInputKey::User {
43        id: NEXT_INPUT_ID.fetch_add(1, Ordering::Relaxed),
44    }
45}
46
47pub(crate) fn next_traced_id() -> TracedTensorId {
48    NEXT_TRACED_ID.fetch_add(1, Ordering::Relaxed)
49}
50
51type TracedInputMap = RetainedInputMap;
52
53#[derive(Clone)]
54pub struct TracedTensor {
55    pub id: TracedTensorId,
56    pub rank: usize,
57    pub dtype: DType,
58    pub(crate) graph: Arc<Graph<StdTensorOp>>,
59    pub val: LocalValueId,
60    pub(crate) data: Option<Arc<RetainedValue>>,
61    pub(crate) shape_hint: Option<Vec<SymDim>>,
62    pub(crate) inputs_map: Arc<TracedInputMap>,
63    /// Retained construction-time metadata per bound leaf input key.
64    ///
65    /// Populated by data-attached leaf constructors with the exact
66    /// `TensorMeta` they register (symbolic `tensor_axis` extents for
67    /// [`TracedTensor::from_shared_tensor_value_symbolic_shape`]), so the
68    /// deferred eager-AD analysis can seed symbolic leaf metadata without
69    /// deriving concrete extents from the bound values.
70    pub(crate) leaf_metas: Arc<HashMap<TensorInputKey, TensorMeta>>,
71    pub(crate) extra_roots: Vec<Arc<Graph<StdTensorOp>>>,
72    pub(crate) checkpoint_chain: Option<Arc<CheckpointNode>>,
73    pub(crate) metadata_scopes: MetadataScopeChain,
74    pub(crate) constraint_scopes: ConstraintScopeChain,
75}
76
77impl fmt::Debug for TracedTensor {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        f.debug_struct("TracedTensor")
80            .field("id", &self.id)
81            .field("rank", &self.rank)
82            .field("dtype", &self.dtype)
83            .field("val", &self.val)
84            .field("shape_hint", &self.shape_hint)
85            .field("has_data", &self.data.is_some())
86            .finish_non_exhaustive()
87    }
88}
89
90pub(crate) fn merge_traced_inputs_map<'a>(
91    inputs: impl IntoIterator<Item = &'a TracedTensor>,
92) -> Arc<TracedInputMap> {
93    let maps: Vec<_> = inputs
94        .into_iter()
95        .map(|input| &input.inputs_map)
96        .filter(|map| !map.is_empty())
97        .collect();
98    match maps.as_slice() {
99        [] => return Arc::new(HashMap::new()),
100        [single] => return Arc::clone(*single),
101        _ => {}
102    }
103
104    for &candidate in &maps {
105        if input_map_matches_ordered_merge(candidate.as_ref(), &maps) {
106            return Arc::clone(candidate);
107        }
108    }
109
110    let mut merged = (**maps[0]).clone();
111    for map in maps.iter().skip(1) {
112        merged.extend(
113            map.iter()
114                .map(|(key, tensor)| (key.clone(), tensor.clone())),
115        );
116    }
117    Arc::new(merged)
118}
119
120fn input_map_matches_ordered_merge(
121    candidate: &TracedInputMap,
122    maps: &[&Arc<TracedInputMap>],
123) -> bool {
124    merged_map_matches_ordered(candidate, maps, Arc::ptr_eq)
125}
126
127/// Merge the retained leaf-metadata maps of several traced tensors into one.
128///
129/// Sibling of [`merge_traced_inputs_map`]: the merged map covers exactly the
130/// same leaf keys as the merged bindings map, mapping each to the
131/// construction-time `TensorMeta` its leaf registered (symbolic for symbolic
132/// leaves).
133pub(crate) fn merge_traced_leaf_metas<'a>(
134    inputs: impl IntoIterator<Item = &'a TracedTensor>,
135) -> Arc<HashMap<TensorInputKey, TensorMeta>> {
136    let maps: Vec<_> = inputs
137        .into_iter()
138        .map(|input| &input.leaf_metas)
139        .filter(|map| !map.is_empty())
140        .collect();
141    match maps.as_slice() {
142        [] => return Arc::new(HashMap::new()),
143        [single] => return Arc::clone(*single),
144        _ => {}
145    }
146
147    for &candidate in &maps {
148        if merged_map_matches_ordered(candidate.as_ref(), &maps, |a, b| a == b) {
149            return Arc::clone(candidate);
150        }
151    }
152
153    let mut merged = (**maps[0]).clone();
154    for map in maps.iter().skip(1) {
155        merged.extend(map.iter().map(|(key, meta)| (key.clone(), meta.clone())));
156    }
157    Arc::new(merged)
158}
159
160fn merged_map_matches_ordered<V>(
161    candidate: &HashMap<TensorInputKey, V>,
162    maps: &[&Arc<HashMap<TensorInputKey, V>>],
163    matches: impl Fn(&V, &V) -> bool,
164) -> bool {
165    for map in maps {
166        for key in map.keys() {
167            let Some(final_value) = maps.iter().rev().find_map(|source| source.get(key)) else {
168                return false;
169            };
170            let Some(candidate_value) = candidate.get(key) else {
171                return false;
172            };
173            if !matches(candidate_value, final_value) {
174                return false;
175            }
176        }
177    }
178    true
179}
180
181pub(crate) fn try_concrete_shape(tensor: &TracedTensor) -> Option<Vec<usize>> {
182    tensor
183        .shape_hint
184        .as_ref()?
185        .iter()
186        .map(SymDim::constant_value)
187        .collect()
188}
189
190fn graph_validation(op: &'static str, source: impl Into<ValidationError>) -> Error {
191    Error::validation(op, ErrorPhase::GraphBuild, source.into())
192}
193
194fn graph_invalid_argument(
195    op: &'static str,
196    argument: &'static str,
197    message: impl Into<String>,
198) -> Error {
199    graph_validation(
200        op,
201        ValidationError::InvalidArgument {
202            argument,
203            message: message.into(),
204        },
205    )
206}
207
208fn graph_broadcast_error(op: &'static str, error: BroadcastError) -> Error {
209    graph_validation(op, broadcast_error_to_validation(error))
210}
211
212fn graph_tensor_error(op: &'static str, error: TensorError) -> Error {
213    match error {
214        TensorError::Validation { source, .. } => graph_validation(op, source),
215        other => Error::TensorRuntime(other),
216    }
217}
218
219fn graph_error_with_context(op: &'static str, error: Error) -> Error {
220    match error {
221        Error::Validation { source, .. } => graph_validation(op, source),
222        other => other,
223    }
224}
225
226pub(crate) fn concrete_shape(tensor: &TracedTensor) -> Result<Vec<usize>> {
227    tensor
228        .shape_hint
229        .as_ref()
230        .ok_or_else(|| {
231            graph_invalid_argument(
232                "TracedTensor::concrete_shape",
233                "shape",
234                format!("missing shape hint for traced tensor {}", tensor.id),
235            )
236        })?
237        .iter()
238        .map(|dim| {
239            dim.constant_value().ok_or_else(|| {
240                graph_invalid_argument(
241                    "TracedTensor::concrete_shape",
242                    "shape",
243                    format!("symbolic dimension in shape hint for tensor {}", tensor.id),
244                )
245            })
246        })
247        .collect()
248}
249
250/// Broadcast a traced tensor to `target_shape`.
251///
252/// Expanding singleton axes are first reshaped away so the existing
253/// `BroadcastInDim` transpose rule reduces them correctly during VJP.
254pub(crate) fn broadcast_to(tensor: &TracedTensor, target_shape: &[usize]) -> Result<TracedTensor> {
255    let tensor_shape = concrete_shape(tensor)?;
256    if tensor_shape == target_shape {
257        return Ok(tensor.clone());
258    }
259
260    let plan = broadcast_input_plan(&tensor_shape, target_shape)
261        .map_err(|err| graph_broadcast_error("broadcast_to", err))?;
262
263    let source = if plan.source_shape == tensor_shape {
264        tensor.clone()
265    } else {
266        tensor.reshape(&plan.source_shape)?
267    };
268    source.broadcast_in_dim(target_shape, &plan.dims)
269}
270
271/// Broadcast two tensors to a common shape.
272pub(crate) fn broadcast_binary(
273    a: &TracedTensor,
274    b: &TracedTensor,
275) -> Result<(TracedTensor, TracedTensor)> {
276    if a.shape_hint == b.shape_hint && a.rank == b.rank {
277        return Ok((a.clone(), b.clone()));
278    }
279    if (try_concrete_shape(a).is_none() || try_concrete_shape(b).is_none()) && a.rank == b.rank {
280        return Ok((a.clone(), b.clone()));
281    }
282    let a_shape = concrete_shape(a)?;
283    let b_shape = concrete_shape(b)?;
284    let target = broadcast_shape(&a_shape, &b_shape)
285        .map_err(|err| graph_broadcast_error("broadcast_binary", err))?;
286    Ok((broadcast_to(a, &target)?, broadcast_to(b, &target)?))
287}
288
289pub(crate) fn broadcast_ternary(
290    a: &TracedTensor,
291    b: &TracedTensor,
292    c: &TracedTensor,
293) -> Result<(TracedTensor, TracedTensor, TracedTensor)> {
294    let a_shape = concrete_shape(a)?;
295    let b_shape = concrete_shape(b)?;
296    let c_shape = concrete_shape(c)?;
297    let target = broadcast_shapes([a_shape.as_slice(), b_shape.as_slice(), c_shape.as_slice()])
298        .map_err(|err| graph_broadcast_error("broadcast_ternary", err))?;
299    Ok((
300        broadcast_to(a, &target)?,
301        broadcast_to(b, &target)?,
302        broadcast_to(c, &target)?,
303    ))
304}
305
306fn scale_with_constant(input: &TracedTensor, op: StdTensorOp) -> Result<TracedTensor> {
307    let scalar = apply_nullary(op, 0, input.dtype, Some(vec![]))?;
308    apply_binary(
309        StdTensorOp::Mul,
310        input,
311        &scalar,
312        input.rank,
313        input.shape_hint.clone(),
314    )
315}
316
317fn try_inferred_output_dtype(
318    op: &StdTensorOp,
319    inputs: &[DType],
320    context: &'static str,
321) -> Result<DType> {
322    crate::shape_infer::infer_output_dtype_at(op, inputs, ErrorPhase::GraphBuild)
323        .map_err(|err| graph_error_with_context(context, err))
324}
325
326fn checked_shape_product_for_graph_build(
327    shape: &[usize],
328    context: &'static str,
329    _role: &'static str,
330) -> Result<usize> {
331    shape.iter().copied().try_fold(1usize, |acc, dim| {
332        acc.checked_mul(dim)
333            .ok_or_else(|| graph_validation(context, ValidationError::IntegerOverflow))
334    })
335}
336
337fn validate_concrete_reshape_shape(input: &TracedTensor, shape: &[usize]) -> Result<()> {
338    let to = checked_shape_product_for_graph_build(shape, "TracedTensor::reshape", "target")?;
339    let Some(input_shape) = try_concrete_shape(input) else {
340        return Ok(());
341    };
342    let from =
343        checked_shape_product_for_graph_build(&input_shape, "TracedTensor::reshape", "input")?;
344    if from != to {
345        return Err(graph_validation(
346            "TracedTensor::reshape",
347            ShapeMismatch::ReshapeElementCount { from, to },
348        ));
349    }
350    Ok(())
351}
352
353fn traced_input_shape_exprs(input_idx: usize, tensor: &TracedTensor) -> Vec<DimExpr> {
354    match tensor.shape_hint.as_ref() {
355        Some(shape) => shape
356            .iter()
357            .enumerate()
358            .map(|(axis, dim)| {
359                dim.constant_value()
360                    .map_or(DimExpr::InputDim { input_idx, axis }, DimExpr::Const)
361            })
362            .collect(),
363        None => (0..tensor.rank)
364            .map(|axis| DimExpr::InputDim { input_idx, axis })
365            .collect(),
366    }
367}
368
369fn traced_input_sym_shape(tensor: &TracedTensor) -> Vec<SymDim> {
370    tensor.shape_hint.clone().unwrap_or_else(|| {
371        (0..tensor.rank)
372            .map(|axis| SymDim::tensor_axis(tensor.id, axis))
373            .collect()
374    })
375}
376
377pub(crate) fn infer_traced_single_output_shape(
378    op_name: &'static str,
379    op: &StdTensorOp,
380    inputs: &[&TracedTensor],
381) -> Result<(usize, Option<Vec<SymDim>>)> {
382    let input_shape_exprs: Vec<Vec<DimExpr>> = inputs
383        .iter()
384        .enumerate()
385        .map(|(input_idx, tensor)| traced_input_shape_exprs(input_idx, tensor))
386        .collect();
387    let input_shape_refs: Vec<&[DimExpr]> = input_shape_exprs.iter().map(Vec::as_slice).collect();
388    let output_shapes = crate::shape_infer::infer_output_shapes(op, &input_shape_refs)
389        .map_err(|err| graph_error_with_context(op_name, err))?;
390    let output_shape = output_shapes.first().ok_or_else(|| {
391        Error::Internal(format!("{op_name}: shape inference returned no outputs"))
392    })?;
393    if output_shapes.len() != 1 {
394        return Err(Error::Internal(format!(
395            "{op_name}: expected single-output shape inference, got {} outputs",
396            output_shapes.len()
397        )));
398    }
399
400    let input_sym_shapes: Vec<Vec<SymDim>> = inputs
401        .iter()
402        .map(|tensor| traced_input_sym_shape(tensor))
403        .collect();
404    let input_sym_refs: Vec<&[SymDim]> = input_sym_shapes.iter().map(Vec::as_slice).collect();
405    let out_shape_hint = output_shape
406        .iter()
407        .map(|dim| SymDim::from_dim_expr(dim, &input_sym_refs))
408        .collect();
409    Ok((output_shape.len(), Some(out_shape_hint)))
410}
411
412pub(crate) fn register_metadata_or_runtime_state<E>(
413    result: std::result::Result<GlobalMetadataScope, E>,
414) -> Result<GlobalMetadataScope>
415where
416    E: StdError + Send + Sync + 'static,
417{
418    result.map_err(|err| Error::runtime_state_source("metadata", ErrorPhase::Compile, err))
419}
420
421fn reduction_output_meta(
422    tensor: &TracedTensor,
423    axes: &[usize],
424    op: &'static str,
425) -> Result<(usize, Option<Vec<SymDim>>)> {
426    let mut seen = vec![false; tensor.rank];
427    for &axis in axes {
428        if axis >= tensor.rank {
429            return Err(graph_validation(
430                op,
431                ValidationError::AxisOutOfBounds {
432                    axis,
433                    rank: tensor.rank,
434                },
435            ));
436        }
437        if seen[axis] {
438            return Err(graph_validation(
439                op,
440                ValidationError::DuplicateAxis {
441                    axis,
442                    role: "reduction",
443                },
444            ));
445        }
446        seen[axis] = true;
447    }
448
449    let out_shape_hint = tensor.shape_hint.as_ref().map(|shape| {
450        (0..shape.len())
451            .filter(|d| !axes.contains(d))
452            .map(|d| shape[d].clone())
453            .collect()
454    });
455    Ok((tensor.rank - axes.len(), out_shape_hint))
456}
457
458fn validate_traced_axis(tensor: &TracedTensor, axis: usize, op: &'static str) -> Result<()> {
459    if axis >= tensor.rank {
460        return Err(graph_validation(
461            op,
462            ValidationError::AxisOutOfBounds {
463                axis,
464                rank: tensor.rank,
465            },
466        ));
467    }
468    Ok(())
469}
470
471fn validate_traced_axes(rank: usize, axes: &[usize], op: &'static str) -> Result<()> {
472    let mut seen = vec![false; rank];
473    for &axis in axes {
474        if axis >= rank {
475            return Err(graph_validation(
476                op,
477                ValidationError::AxisOutOfBounds { axis, rank },
478            ));
479        }
480        if seen[axis] {
481            return Err(graph_validation(
482                op,
483                ValidationError::DuplicateAxis { axis, role: "axis" },
484            ));
485        }
486        seen[axis] = true;
487    }
488    Ok(())
489}
490
491fn validate_traced_insert_axis(rank: usize, axis: usize, op: &'static str) -> Result<()> {
492    if axis > rank {
493        return Err(graph_invalid_argument(
494            op,
495            "axis",
496            format!("axis {axis} out of bounds for rank {rank} insertion"),
497        ));
498    }
499    Ok(())
500}
501
502fn validate_traced_perm(rank: usize, perm: &[usize], op: &'static str) -> Result<()> {
503    if perm.len() != rank {
504        return Err(graph_validation(
505            op,
506            ValidationError::InvalidPermutationLength {
507                expected: rank,
508                actual: perm.len(),
509            },
510        ));
511    }
512    let mut seen = vec![false; rank];
513    for &axis in perm {
514        if axis >= rank {
515            return Err(graph_validation(
516                op,
517                ValidationError::AxisOutOfBounds { axis, rank },
518            ));
519        }
520        if seen[axis] {
521            return Err(graph_validation(
522                op,
523                ValidationError::DuplicateAxis {
524                    axis,
525                    role: "permutation",
526                },
527            ));
528        }
529        seen[axis] = true;
530    }
531    Ok(())
532}
533
534fn validate_broadcast_in_dim_args(
535    input: &TracedTensor,
536    output_shape: &[SymDim],
537    dims: &[usize],
538    op: &'static str,
539) -> Result<()> {
540    if dims.len() != input.rank {
541        return Err(graph_validation(
542            op,
543            ValidationError::RankMismatch {
544                expected: input.rank,
545                actual: dims.len(),
546            },
547        ));
548    }
549
550    let concrete_input_shape: Option<Vec<usize>> = input
551        .shape_hint
552        .as_ref()
553        .and_then(|shape| shape.iter().map(SymDim::constant_value).collect());
554    let concrete_output_shape = output_shape
555        .iter()
556        .map(SymDim::constant_value)
557        .collect::<Option<Vec<_>>>();
558    if let (Some(input_shape), Some(output_shape)) = (
559        concrete_input_shape.as_deref(),
560        concrete_output_shape.as_deref(),
561    ) {
562        if let Some(error) = broadcast_in_dim_extent_error(input_shape, output_shape, dims) {
563            return Err(graph_broadcast_error(op, error));
564        }
565    }
566
567    let mut seen = vec![false; output_shape.len()];
568    for &dim in dims {
569        if dim >= output_shape.len() {
570            return Err(graph_validation(
571                op,
572                ValidationError::AxisOutOfBounds {
573                    axis: dim,
574                    rank: output_shape.len(),
575                },
576            ));
577        }
578        if seen[dim] {
579            return Err(graph_validation(
580                op,
581                ValidationError::DuplicateAxis {
582                    axis: dim,
583                    role: "broadcast",
584                },
585            ));
586        }
587        seen[dim] = true;
588    }
589
590    Ok(())
591}
592
593impl std::ops::Add for &TracedTensor {
594    type Output = Result<TracedTensor>;
595
596    fn add(self, rhs: &TracedTensor) -> Result<TracedTensor> {
597        TracedTensor::add(self, rhs)
598    }
599}
600
601impl std::ops::Sub for &TracedTensor {
602    type Output = Result<TracedTensor>;
603
604    fn sub(self, rhs: &TracedTensor) -> Result<TracedTensor> {
605        TracedTensor::sub(self, rhs)
606    }
607}
608
609impl std::ops::Mul for &TracedTensor {
610    type Output = Result<TracedTensor>;
611
612    fn mul(self, rhs: &TracedTensor) -> Result<TracedTensor> {
613        TracedTensor::mul(self, rhs)
614    }
615}
616
617impl std::ops::Mul<f64> for &TracedTensor {
618    type Output = Result<TracedTensor>;
619
620    fn mul(self, rhs: f64) -> Result<TracedTensor> {
621        self.scale_real(rhs)
622    }
623}
624
625impl std::ops::Mul<&TracedTensor> for f64 {
626    type Output = Result<TracedTensor>;
627
628    fn mul(self, rhs: &TracedTensor) -> Result<TracedTensor> {
629        rhs.scale_real(self)
630    }
631}
632
633impl std::ops::Neg for &TracedTensor {
634    type Output = Result<TracedTensor>;
635
636    fn neg(self) -> Self::Output {
637        TracedTensor::neg(self)
638    }
639}
640
641impl std::ops::Div for &TracedTensor {
642    type Output = Result<TracedTensor>;
643
644    fn div(self, rhs: &TracedTensor) -> Result<TracedTensor> {
645        TracedTensor::div(self, rhs)
646    }
647}
648
649impl std::ops::Rem for &TracedTensor {
650    type Output = Result<TracedTensor>;
651
652    fn rem(self, rhs: &TracedTensor) -> Result<TracedTensor> {
653        TracedTensor::rem(self, rhs)
654    }
655}
656
657impl TracedTensor {
658    /// Return the graph that owns this traced tensor's current value.
659    ///
660    /// # Examples
661    ///
662    /// ```
663    /// use tenferro_runtime::TracedTensor;
664    ///
665    /// let x = TracedTensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap();
666    /// let _graph = x.graph();
667    /// ```
668    pub fn graph(&self) -> &Arc<Graph<StdTensorOp>> {
669        &self.graph
670    }
671
672    /// Return the concrete tensor data attached to this traced value, if any.
673    ///
674    /// Placeholder tensors created with `input_concrete_shape` or
675    /// `input_symbolic_shape` have no attached data until execution bindings
676    /// provide it.
677    ///
678    /// # Examples
679    ///
680    /// ```
681    /// use tenferro_runtime::{DType, TracedTensor};
682    ///
683    /// let concrete = TracedTensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap();
684    /// assert!(concrete.attached_value().is_some());
685    ///
686    /// let placeholder = TracedTensor::input_symbolic_shape(DType::F64, 1).unwrap();
687    /// assert!(placeholder.attached_value().is_none());
688    /// ```
689    pub fn attached_value(&self) -> Option<&Arc<RetainedValue>> {
690        self.data.as_ref()
691    }
692
693    /// Build a [`TracedTensor`] leaf from a concrete [`Tensor`], keeping its
694    /// shape as a concrete `shape_hint`.
695    ///
696    /// This is the common constructor when you have concrete tensor data that
697    /// you want to use both for graph building and for evaluation. The
698    /// resulting tensor is treated as a concrete-shape leaf by downstream
699    /// passes (binary einsum decomposition, build-time reshape folding, etc.).
700    ///
701    /// # Examples
702    ///
703    /// ```
704    /// use tenferro_runtime::{Tensor, TracedTensor};
705    ///
706    /// let a = TracedTensor::from_tensor_concrete_shape(
707    ///     Tensor::from_vec_col_major(vec![2, 3], vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap(),
708    /// )
709    /// .unwrap();
710    /// assert_eq!(a.rank, 2);
711    /// assert!(a.is_concrete_shape());
712    /// ```
713    ///
714    /// # Errors
715    ///
716    /// Returns [`Error::RuntimeStateSource`] when graph metadata registration
717    /// cannot retain the concrete tensor's shape or dtype.
718    pub fn from_tensor_concrete_shape(tensor: Tensor) -> Result<Self> {
719        let shape = tensor.shape().to_vec();
720        let rank = shape.len();
721        let dtype = tensor.dtype();
722        let key = next_input_key();
723        let id = next_traced_id();
724        let data = Arc::new(RetainedValue::from_tensor(tensor));
725        let meta = concrete_tensor_meta(dtype, &shape);
726
727        let mut builder = GraphBuilder::new();
728        let val = builder.add_input(key.clone());
729        builder.set_outputs(vec![val]);
730        let graph = Arc::new(builder.build());
731        let metadata_scope = register_metadata_or_runtime_state(register_scoped_value_metadata(
732            graph.values()[val].key.clone(),
733            meta.clone(),
734        ))?;
735
736        let mut map = HashMap::new();
737        map.insert(key.clone(), Arc::clone(&data));
738        let mut leaf_metas = HashMap::new();
739        leaf_metas.insert(key, meta);
740
741        Ok(Self {
742            id,
743            rank,
744            dtype,
745            graph,
746            val,
747            data: Some(data),
748            shape_hint: Some(shape.into_iter().map(SymDim::from).collect()),
749            inputs_map: Arc::new(map),
750            leaf_metas: Arc::new(leaf_metas),
751            extra_roots: Vec::new(),
752            checkpoint_chain: None,
753            metadata_scopes: MetadataScopeChain::from_scope(metadata_scope),
754            constraint_scopes: ConstraintScopeChain::empty(),
755        })
756    }
757
758    /// Build a [`TracedTensor`] leaf from a concrete [`Tensor`] but advertise
759    /// a symbolic shape during graph construction.
760    ///
761    /// The tensor data is still attached (so plain `eval` works without
762    /// bindings), but graph passes see the leaf as shape-symbolic. This is
763    /// useful for building a single traced program that should not bake in
764    /// shape-specific optimizations.
765    ///
766    /// # Examples
767    ///
768    /// ```
769    /// use tenferro_runtime::{Tensor, TracedTensor};
770    ///
771    /// let t = TracedTensor::from_tensor_symbolic_shape(
772    ///     Tensor::from_vec_col_major(vec![2, 3], vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap(),
773    /// )
774    /// .unwrap();
775    /// assert_eq!(t.rank, 2);
776    /// assert!(!t.is_concrete_shape());
777    /// ```
778    ///
779    /// # Errors
780    ///
781    /// Returns [`Error::RuntimeStateSource`] when symbolic graph metadata
782    /// registration is unavailable or its registry state is poisoned.
783    pub fn from_tensor_symbolic_shape(tensor: Tensor) -> Result<Self> {
784        Self::from_tensor_value_symbolic_shape(TensorValue::from_tensor(tensor))
785    }
786
787    /// Build a data-attached symbolic-shape traced leaf from shared tensor data.
788    ///
789    /// This is an internal crate-boundary helper for eager AD recording. It
790    /// preserves the same tensor allocation used by the eager value while graph
791    /// passes see symbolic input extents.
792    ///
793    /// # Examples
794    ///
795    /// ```
796    /// use std::sync::Arc;
797    /// use tenferro_runtime::{Tensor, TracedTensor};
798    ///
799    /// let tensor = Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
800    /// let traced = TracedTensor::from_tensor_symbolic_shape(tensor)?;
801    /// assert_eq!(traced.rank, 1);
802    /// assert!(!traced.is_concrete_shape());
803    /// # Ok::<(), tenferro_runtime::Error>(())
804    /// ```
805    ///
806    /// # Errors
807    ///
808    /// Returns [`Error::RuntimeStateSource`] when symbolic graph metadata
809    /// registration is unavailable or its registry state is poisoned.
810    #[doc(hidden)]
811    pub fn from_tensor_value_symbolic_shape(value: TensorValue) -> Result<Self> {
812        let data = Arc::new(RetainedValue::from_tensor_value(value)?);
813        Self::from_shared_tensor_value_symbolic_shape(data)
814    }
815
816    #[doc(hidden)]
817    pub fn from_shared_tensor_value_symbolic_shape(data: Arc<RetainedValue>) -> Result<Self> {
818        let rank = data.shape().len();
819        let dtype = data.dtype();
820        let key = next_input_key();
821        let id = next_traced_id();
822        let meta = symbolic_input_meta(dtype, id, rank);
823
824        let mut builder = GraphBuilder::new();
825        let val = builder.add_input(key.clone());
826        builder.set_outputs(vec![val]);
827        let graph = Arc::new(builder.build());
828        let metadata_scope = register_metadata_or_runtime_state(register_scoped_value_metadata(
829            graph.values()[val].key.clone(),
830            meta.clone(),
831        ))?;
832
833        let mut map = HashMap::new();
834        map.insert(key.clone(), Arc::clone(&data));
835        let mut leaf_metas = HashMap::new();
836        leaf_metas.insert(key, meta);
837
838        Ok(Self {
839            id,
840            rank,
841            dtype,
842            graph,
843            val,
844            data: Some(data),
845            shape_hint: None,
846            inputs_map: Arc::new(map),
847            leaf_metas: Arc::new(leaf_metas),
848            extra_roots: Vec::new(),
849            checkpoint_chain: None,
850            metadata_scopes: MetadataScopeChain::from_scope(metadata_scope),
851            constraint_scopes: ConstraintScopeChain::empty(),
852        })
853    }
854
855    /// Build a data-less placeholder leaf with a fixed (concrete) shape.
856    ///
857    /// Must be passed as an input to [`crate::Runtime::run_compiled`] before evaluation.
858    /// Use this when you know the exact shape of the input but want to build
859    /// the graph once and feed different concrete tensors at execution time.
860    ///
861    /// # Examples
862    ///
863    /// ```
864    /// use tenferro_tensor::DType;
865    /// use tenferro_runtime::TracedTensor;
866    ///
867    /// let x = TracedTensor::input_concrete_shape(DType::F64, &[2, 3]).unwrap();
868    /// assert_eq!(x.rank, 2);
869    /// assert!(x.is_concrete_shape());
870    /// ```
871    ///
872    /// # Errors
873    ///
874    /// Returns [`Error::RuntimeStateSource`] when graph metadata registration
875    /// fails or the registry state is poisoned. `dtype` and `shape` are
876    /// metadata values and are not revalidated by this constructor.
877    pub fn input_concrete_shape(dtype: DType, shape: &[usize]) -> Result<Self> {
878        let shape = shape.to_vec();
879        let rank = shape.len();
880        let key = next_input_key();
881        let id = next_traced_id();
882
883        let mut builder = GraphBuilder::new();
884        let val = builder.add_input(key.clone());
885        builder.set_outputs(vec![val]);
886        let graph = Arc::new(builder.build());
887        let metadata_scope = register_metadata_or_runtime_state(register_scoped_value_metadata(
888            graph.values()[val].key.clone(),
889            concrete_tensor_meta(dtype, &shape),
890        ))?;
891
892        Ok(Self {
893            id,
894            rank,
895            dtype,
896            graph,
897            val,
898            data: None,
899            shape_hint: Some(shape.into_iter().map(SymDim::from).collect()),
900            inputs_map: Arc::new(HashMap::new()),
901            leaf_metas: Arc::new(HashMap::new()),
902            extra_roots: Vec::new(),
903            checkpoint_chain: None,
904            metadata_scopes: MetadataScopeChain::from_scope(metadata_scope),
905            constraint_scopes: ConstraintScopeChain::empty(),
906        })
907    }
908
909    /// Build a data-less placeholder leaf with the given rank but fully
910    /// symbolic shape (every dim is a distinct `SymDim::TensorAxis`).
911    ///
912    /// Must be passed as an input to [`crate::Runtime::run_compiled`] before
913    /// evaluation. Use this to build shape-agnostic graphs.
914    ///
915    /// # Examples
916    ///
917    /// ```
918    /// use tenferro_tensor::DType;
919    /// use tenferro_runtime::TracedTensor;
920    ///
921    /// let x = TracedTensor::input_symbolic_shape(DType::F64, 2).unwrap();
922    /// assert_eq!(x.rank, 2);
923    /// assert!(!x.is_concrete_shape());
924    /// ```
925    ///
926    /// # Errors
927    ///
928    /// Returns [`Error::RuntimeStateSource`] when graph metadata registration
929    /// fails or the registry state is poisoned. `rank` is recorded as the
930    /// symbolic placeholder rank and is not otherwise rejected here.
931    pub fn input_symbolic_shape(dtype: DType, rank: usize) -> Result<Self> {
932        let key = next_input_key();
933        let id = next_traced_id();
934
935        let mut builder = GraphBuilder::new();
936        let val = builder.add_input(key.clone());
937        builder.set_outputs(vec![val]);
938        let graph = Arc::new(builder.build());
939        let metadata_scope = register_metadata_or_runtime_state(register_scoped_value_metadata(
940            graph.values()[val].key.clone(),
941            symbolic_input_meta(dtype, id, rank),
942        ))?;
943
944        Ok(Self {
945            id,
946            rank,
947            dtype,
948            graph,
949            val,
950            data: None,
951            shape_hint: None,
952            inputs_map: Arc::new(HashMap::new()),
953            leaf_metas: Arc::new(HashMap::new()),
954            extra_roots: Vec::new(),
955            checkpoint_chain: None,
956            metadata_scopes: MetadataScopeChain::from_scope(metadata_scope),
957            constraint_scopes: ConstraintScopeChain::empty(),
958        })
959    }
960
961    /// Build a concrete-shape [`TracedTensor`] leaf from column-major typed
962    /// `Vec<T>` data.
963    ///
964    /// The data must already be in tenferro's physical column-major order.
965    ///
966    /// # Examples
967    ///
968    /// ```
969    /// use tenferro_runtime::TracedTensor;
970    ///
971    /// let a = TracedTensor::from_vec_col_major(
972    ///     vec![2, 3],
973    ///     vec![1.0_f64, 4.0, 2.0, 5.0, 3.0, 6.0],
974    /// )?;
975    /// assert_eq!(a.rank, 2);
976    /// # Ok::<(), tenferro_runtime::Error>(())
977    /// ```
978    ///
979    /// # Errors
980    ///
981    /// Returns [`Error::TensorRuntime`] containing
982    /// `ValidationError::ShapeDataLengthMismatch` when the shape product does
983    /// not equal `data.len()`, or `ValidationError::IntegerOverflow` when the
984    /// shape product cannot be represented by `usize`.
985    pub fn from_vec_col_major<T: TensorScalar>(
986        shape: impl IntoShapeVec,
987        data: Vec<T>,
988    ) -> Result<Self> {
989        Self::from_tensor_concrete_shape(Tensor::from_vec_col_major(shape, data)?)
990    }
991
992    /// Return the tensor element dtype recorded for this traced value.
993    pub fn dtype(&self) -> DType {
994        self.dtype
995    }
996
997    /// Returns `true` iff every dim of this tensor's `shape_hint` is a
998    /// constant `SymDim` (i.e. the shape is fully known at graph-build time).
999    ///
1000    /// # Examples
1001    ///
1002    /// ```
1003    /// use tenferro_tensor::DType;
1004    /// use tenferro_runtime::TracedTensor;
1005    ///
1006    /// let a = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
1007    /// let b = TracedTensor::input_symbolic_shape(DType::F64, 2).unwrap();
1008    /// assert!(a.is_concrete_shape());
1009    /// assert!(!b.is_concrete_shape());
1010    /// ```
1011    pub fn is_concrete_shape(&self) -> bool {
1012        try_concrete_shape(self).is_some()
1013    }
1014
1015    /// Return the fully-concrete shape of this tensor, if every dim of
1016    /// its shape-hint is a constant `SymDim`. Returns `None` if any
1017    /// dimension is symbolic.
1018    ///
1019    /// This is the counterpart to [`Self::is_concrete_shape`] for callers
1020    /// that need to *use* the concrete shape (e.g. external composition
1021    /// wrappers building `broadcast_in_dim` payloads from known shapes).
1022    ///
1023    /// # Examples
1024    ///
1025    /// ```
1026    /// use tenferro_tensor::DType;
1027    /// use tenferro_runtime::TracedTensor;
1028    ///
1029    /// let a = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
1030    /// assert_eq!(a.try_concrete_shape(), Some(vec![2, 3]));
1031    ///
1032    /// let b = TracedTensor::input_symbolic_shape(DType::F64, 2).unwrap();
1033    /// assert!(b.try_concrete_shape().is_none());
1034    /// ```
1035    pub fn try_concrete_shape(&self) -> Option<Vec<usize>> {
1036        try_concrete_shape(self)
1037    }
1038
1039    /// Return the concrete tensor shape.
1040    ///
1041    /// Returns an error when a shape hint is missing or any dimension is
1042    /// symbolic. Composite traced ops that require concrete sizes should
1043    /// propagate this error instead of panicking.
1044    ///
1045    /// # Errors
1046    ///
1047    /// Returns [`Error::Validation`] with `InvalidArgument` when this tensor
1048    /// has no shape hint or any dimension is symbolic.
1049    pub fn concrete_shape(&self) -> Result<Vec<usize>> {
1050        concrete_shape(self)
1051    }
1052
1053    /// If this `TracedTensor` is a leaf (single-node input graph),
1054    /// return its input key. Computed tensors return `None`.
1055    pub fn input_key(&self) -> Option<TensorInputKey> {
1056        match &self.graph.values()[self.val].key {
1057            ValueKey::Input(key) => Some(key.clone()),
1058            _ => None,
1059        }
1060    }
1061
1062    /// Return whether this traced graph carries default data for `key`.
1063    #[doc(hidden)]
1064    pub fn has_attached_input_key(&self, key: &TensorInputKey) -> bool {
1065        self.inputs_map.contains_key(key)
1066    }
1067
1068    /// Elementwise addition with NumPy-style broadcasting.
1069    ///
1070    /// Prefer using the `+` operator when it reads naturally.
1071    ///
1072    /// A longer expression such as `a + b + c` does not compose because the
1073    /// first `+` returns `Result<TracedTensor, Error>`, so the second `+`
1074    /// would receive a result rather than a tensor. Use `?` at each step or
1075    /// the explicit fallible method chain shown below when the operation
1076    /// sequence is more important than notation:
1077    ///
1078    /// # Examples
1079    ///
1080    /// ```rust
1081    /// # use tenferro_runtime::{Error, TracedTensor};
1082    /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1083    /// # let z = TracedTensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap();
1084    /// let y = x.add(&z);
1085    /// let y2 = &x + &z;
1086    /// # fn add_three(
1087    /// #     a: &TracedTensor,
1088    /// #     b: &TracedTensor,
1089    /// #     c: &TracedTensor,
1090    /// # ) -> Result<TracedTensor, Error> {
1091    /// let ab = (a + b)?;
1092    /// let sum = (&ab + c)?;
1093    /// let method_chain = a.add(b)?.add(c)?;
1094    /// let _ = method_chain;
1095    /// # Ok(sum)
1096    /// # }
1097    /// ```
1098    ///
1099    /// Tenferro prioritizes robust error handling over the conciseness of
1100    /// chained operator notation; the explicit fallible methods are the
1101    /// canonical form for longer sequences.
1102    ///
1103    /// # Errors
1104    ///
1105    /// Returns [`Error::Validation`] with `ShapeMismatch` when operand shapes
1106    /// cannot be broadcast, or [`Error::RuntimeStateSource`] when graph
1107    /// metadata registration fails.
1108    ///
1109    /// # Deferred errors
1110    ///
1111    /// If symbolic dimensions prevent shape comparison during graph
1112    /// construction, the same `ShapeMismatch` can be reported during
1113    /// compilation or execution, with the corresponding [`ErrorPhase`].
1114    pub fn add(&self, other: &TracedTensor) -> Result<TracedTensor> {
1115        let (lhs, rhs) = broadcast_binary(self, other)?;
1116        apply_binary(
1117            StdTensorOp::Add,
1118            &lhs,
1119            &rhs,
1120            lhs.rank,
1121            lhs.shape_hint.clone(),
1122        )
1123    }
1124
1125    /// Elementwise subtraction with NumPy-style broadcasting.
1126    ///
1127    /// Prefer using the `-` operator when it reads naturally.
1128    ///
1129    /// # Errors
1130    ///
1131    /// Returns [`Error::Validation`] with `ShapeMismatch` when operand shapes
1132    /// cannot be broadcast, or [`Error::RuntimeStateSource`] when graph
1133    /// metadata registration fails.
1134    ///
1135    /// # Deferred errors
1136    ///
1137    /// If symbolic dimensions prevent shape comparison during graph
1138    /// construction, the same `ShapeMismatch` can be reported during
1139    /// compilation or execution, with the corresponding [`ErrorPhase`].
1140    pub fn sub(&self, other: &TracedTensor) -> Result<TracedTensor> {
1141        let (lhs, rhs) = broadcast_binary(self, other)?;
1142        apply_binary(
1143            StdTensorOp::Sub,
1144            &lhs,
1145            &rhs,
1146            lhs.rank,
1147            lhs.shape_hint.clone(),
1148        )
1149    }
1150
1151    /// Elementwise multiplication with NumPy-style broadcasting.
1152    ///
1153    /// Prefer using the `*` operator when it reads naturally.
1154    ///
1155    /// # Examples
1156    ///
1157    /// ```rust
1158    /// # use tenferro_runtime::TracedTensor;
1159    /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1160    /// # let z = TracedTensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap();
1161    /// let y = x.mul(&z);
1162    /// let y2 = &x * &z;
1163    /// ```
1164    ///
1165    /// # Errors
1166    ///
1167    /// Returns [`Error::Validation`] with `ShapeMismatch` when operand shapes
1168    /// cannot be broadcast, or [`Error::RuntimeStateSource`] when graph
1169    /// metadata registration fails.
1170    ///
1171    /// # Deferred errors
1172    ///
1173    /// If symbolic ranks prevent shape comparison during graph construction,
1174    /// the same `ShapeMismatch` can be reported during compilation or
1175    /// execution, with the corresponding [`ErrorPhase`].
1176    pub fn mul(&self, other: &TracedTensor) -> Result<TracedTensor> {
1177        let (lhs, rhs) = broadcast_binary(self, other)?;
1178        apply_binary(
1179            StdTensorOp::Mul,
1180            &lhs,
1181            &rhs,
1182            lhs.rank,
1183            lhs.shape_hint.clone(),
1184        )
1185    }
1186
1187    /// Elementwise division with NumPy-style broadcasting.
1188    ///
1189    /// Prefer using the `/` operator when it reads naturally.
1190    ///
1191    /// # Examples
1192    ///
1193    /// ```rust
1194    /// # use tenferro_runtime::TracedTensor;
1195    /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1196    /// # let z = TracedTensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap();
1197    /// let y = x.div(&z);
1198    /// let y2 = &x / &z;
1199    /// ```
1200    ///
1201    /// # Errors
1202    ///
1203    /// Returns [`Error::Validation`] with `ShapeMismatch` when operand shapes
1204    /// cannot be broadcast, or [`Error::RuntimeStateSource`] when graph
1205    /// metadata registration fails.
1206    ///
1207    /// # Deferred errors
1208    ///
1209    /// If symbolic ranks prevent shape comparison during graph construction,
1210    /// the same `ShapeMismatch` can be reported during compilation or
1211    /// execution, with the corresponding [`ErrorPhase`]. For integer inputs,
1212    /// a zero divisor is reported during execution as
1213    /// [`Error::TensorRuntime`] containing a
1214    /// [`tenferro_tensor::Error::Extension`] classified as
1215    /// `tenferro_tensor::ErrorKind::NumericalFailure` and retaining the typed
1216    /// backend source; floating-point and complex zero divisors follow their
1217    /// numeric semantics instead.
1218    pub fn div(&self, other: &TracedTensor) -> Result<TracedTensor> {
1219        let (lhs, rhs) = broadcast_binary(self, other)?;
1220        apply_binary(
1221            StdTensorOp::Div,
1222            &lhs,
1223            &rhs,
1224            lhs.rank,
1225            lhs.shape_hint.clone(),
1226        )
1227    }
1228
1229    /// Elementwise remainder with NumPy-style broadcasting.
1230    ///
1231    /// Prefer using the `%` operator when it reads naturally.
1232    ///
1233    /// # Errors
1234    ///
1235    /// Returns [`Error::Validation`] with `ShapeMismatch` when operand shapes
1236    /// cannot be broadcast, [`Error::Unsupported`] at
1237    /// [`ErrorPhase::GraphBuild`] when either operand has a complex dtype, or
1238    /// [`Error::RuntimeStateSource`] when graph metadata registration fails.
1239    ///
1240    /// # Deferred errors
1241    ///
1242    /// If symbolic ranks prevent shape comparison during graph construction,
1243    /// the same `ShapeMismatch` can be reported during compilation or
1244    /// execution, with the corresponding [`ErrorPhase`]. For integer inputs,
1245    /// a zero divisor is reported during execution as
1246    /// [`Error::TensorRuntime`] containing a
1247    /// [`tenferro_tensor::Error::Extension`] classified as
1248    /// `tenferro_tensor::ErrorKind::NumericalFailure` and retaining the typed
1249    /// backend source; floating-point zero divisors follow their numeric
1250    /// semantics.
1251    pub fn rem(&self, other: &TracedTensor) -> Result<TracedTensor> {
1252        let (lhs, rhs) = broadcast_binary(self, other)?;
1253        apply_binary(
1254            StdTensorOp::Rem,
1255            &lhs,
1256            &rhs,
1257            lhs.rank,
1258            lhs.shape_hint.clone(),
1259        )
1260    }
1261
1262    /// Elementwise comparison with NumPy-style broadcasting.
1263    ///
1264    /// # Errors
1265    ///
1266    /// Returns [`Error::Validation`] with `ShapeMismatch` when the concrete
1267    /// operands cannot be broadcast, [`Error::Unsupported`] when ordered
1268    /// comparison rejects a complex dtype, or [`Error::RuntimeStateSource`]
1269    /// when result metadata cannot be registered.
1270    ///
1271    /// # Deferred errors
1272    ///
1273    /// With same-rank symbolic operands, shape compatibility is retained as a
1274    /// graph constraint. A concrete mismatch is reported later as
1275    /// [`Error::TensorRuntime`] containing a typed validation source, with the
1276    /// failure phase identifying compilation or execution.
1277    pub fn compare(&self, other: &TracedTensor, dir: CompareDir) -> Result<TracedTensor> {
1278        apply_broadcast_binary_op(StdTensorOp::Compare(dir), self, other)
1279    }
1280
1281    /// Elementwise maximum with NumPy-style broadcasting.
1282    ///
1283    /// # Errors
1284    ///
1285    /// Returns [`Error::Validation`] with `ShapeMismatch` when the concrete
1286    /// operands cannot be broadcast, [`Error::Unsupported`] when ordered
1287    /// maximum rejects a complex dtype, or [`Error::RuntimeStateSource`] when
1288    /// result metadata cannot be registered.
1289    ///
1290    /// # Deferred errors
1291    ///
1292    /// With same-rank symbolic operands, the broadcast constraint may fail at
1293    /// compile or execution and is returned as [`Error::TensorRuntime`] with
1294    /// its typed validation source.
1295    pub fn maximum(&self, other: &TracedTensor) -> Result<TracedTensor> {
1296        apply_broadcast_binary_op(StdTensorOp::Maximum, self, other)
1297    }
1298
1299    /// Elementwise minimum with NumPy-style broadcasting.
1300    ///
1301    /// # Errors
1302    ///
1303    /// Returns [`Error::Validation`] with `ShapeMismatch` when the concrete
1304    /// operands cannot be broadcast, [`Error::Unsupported`] when ordered
1305    /// minimum rejects a complex dtype, or [`Error::RuntimeStateSource`] when
1306    /// result metadata cannot be registered.
1307    ///
1308    /// # Deferred errors
1309    ///
1310    /// With same-rank symbolic operands, the broadcast constraint may fail at
1311    /// compile or execution and is returned as [`Error::TensorRuntime`] with
1312    /// its typed validation source.
1313    pub fn minimum(&self, other: &TracedTensor) -> Result<TracedTensor> {
1314        apply_broadcast_binary_op(StdTensorOp::Minimum, self, other)
1315    }
1316
1317    /// Select values from `on_true` or `on_false` using `condition`.
1318    ///
1319    /// # Errors
1320    ///
1321    /// Returns [`Error::Validation`] with `InvalidArgument` when an operand
1322    /// lacks concrete shape metadata, or `ShapeMismatch` when the concrete
1323    /// condition and branches cannot share a broadcast shape. Dtype promotion
1324    /// failures are returned as [`Error::TensorRuntime`] with the typed
1325    /// `UnsupportedDTypeConversion` source; metadata failures retain
1326    /// [`Error::RuntimeStateSource`].
1327    pub fn where_select(
1328        condition: &TracedTensor,
1329        on_true: &TracedTensor,
1330        on_false: &TracedTensor,
1331    ) -> Result<TracedTensor> {
1332        apply_broadcast_ternary_op(StdTensorOp::Select, condition, on_true, on_false)
1333    }
1334
1335    /// Alias for [`Self::where_select`].
1336    ///
1337    /// # Errors
1338    ///
1339    /// Returns the same concrete failures as [`Self::where_select`]:
1340    /// [`Error::Validation`] with `InvalidArgument`/`ShapeMismatch` for shape
1341    /// metadata or broadcasting, [`Error::TensorRuntime`] with
1342    /// `UnsupportedDTypeConversion` for failed promotion, and
1343    /// [`Error::RuntimeStateSource`] for metadata registration.
1344    pub fn select(
1345        condition: &TracedTensor,
1346        on_true: &TracedTensor,
1347        on_false: &TracedTensor,
1348    ) -> Result<TracedTensor> {
1349        Self::where_select(condition, on_true, on_false)
1350    }
1351
1352    /// Clamp values elementwise between lower and upper bounds.
1353    ///
1354    /// # Errors
1355    ///
1356    /// Returns [`Error::Validation`] with `InvalidArgument` when an operand
1357    /// lacks concrete shape metadata, `ShapeMismatch` when bounds cannot be
1358    /// broadcast with the input, [`Error::Unsupported`] for an ordered
1359    /// complex dtype, or [`Error::RuntimeStateSource`] when metadata cannot be
1360    /// registered.
1361    pub fn clamp(&self, lower: &TracedTensor, upper: &TracedTensor) -> Result<TracedTensor> {
1362        apply_broadcast_ternary_op(StdTensorOp::Clamp, self, lower, upper)
1363    }
1364
1365    fn apply_same_shape_unary(&self, op: StdTensorOp) -> Result<TracedTensor> {
1366        apply_unary(op, self, self.rank, self.shape_hint.clone())
1367    }
1368
1369    /// Elementwise negation.
1370    ///
1371    /// Prefer using the unary `-` operator when it reads naturally.
1372    ///
1373    /// # Examples
1374    ///
1375    /// ```rust
1376    /// # use tenferro_runtime::TracedTensor;
1377    /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1378    /// let y = x.neg().unwrap();
1379    /// let y2 = (-&x).unwrap();
1380    /// ```
1381    ///
1382    /// # Errors
1383    ///
1384    /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1385    /// is unavailable or poisoned while recording the unary result.
1386    pub fn neg(&self) -> Result<TracedTensor> {
1387        self.apply_same_shape_unary(StdTensorOp::Neg)
1388    }
1389
1390    /// Elementwise complex conjugate.
1391    ///
1392    /// # Examples
1393    ///
1394    /// ```rust
1395    /// # use num_complex::Complex64;
1396    /// # use tenferro_runtime::TracedTensor;
1397    /// # let x = TracedTensor::from_vec_col_major(
1398    /// #     vec![2],
1399    /// #     vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, 4.0)],
1400    /// # )
1401    /// # .unwrap();
1402    /// let y = x.conj().unwrap();
1403    /// ```
1404    ///
1405    /// # Errors
1406    ///
1407    /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1408    /// is unavailable or poisoned while recording the unary result.
1409    pub fn conj(&self) -> Result<TracedTensor> {
1410        self.apply_same_shape_unary(StdTensorOp::Conj)
1411    }
1412
1413    /// Elementwise absolute value.
1414    ///
1415    /// Complex inputs return real magnitudes (`C32 -> F32`, `C64 -> F64`).
1416    ///
1417    /// # Examples
1418    ///
1419    /// ```rust
1420    /// # use tenferro_runtime::TracedTensor;
1421    /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![-1.0_f64, 2.0]).unwrap();
1422    /// let y = x.abs().unwrap();
1423    /// ```
1424    ///
1425    /// # Errors
1426    ///
1427    /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1428    /// is unavailable or poisoned while recording the unary result.
1429    pub fn abs(&self) -> Result<TracedTensor> {
1430        self.apply_same_shape_unary(StdTensorOp::Abs)
1431    }
1432
1433    /// Elementwise sign.
1434    ///
1435    /// # Examples
1436    ///
1437    /// ```rust
1438    /// # use tenferro_runtime::TracedTensor;
1439    /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![-1.0_f64, 2.0]).unwrap();
1440    /// let y = x.sign().unwrap();
1441    /// ```
1442    ///
1443    /// # Errors
1444    ///
1445    /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1446    /// is unavailable or poisoned while recording the unary result.
1447    pub fn sign(&self) -> Result<TracedTensor> {
1448        self.apply_same_shape_unary(StdTensorOp::Sign)
1449    }
1450
1451    /// Scale by a real scalar: `y = factor * x`.
1452    ///
1453    /// # Examples
1454    ///
1455    /// ```rust
1456    /// # use tenferro_runtime::TracedTensor;
1457    /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1458    /// let y = x.scale_real(2.0)?;
1459    /// # Ok::<(), tenferro_runtime::Error>(())
1460    /// ```
1461    ///
1462    /// # Errors
1463    ///
1464    /// Returns [`Error::Validation`] with `InvalidArgument` when an integer or
1465    /// boolean factor is non-finite or out of range for the input dtype, or
1466    /// [`Error::RuntimeStateSource`] when output metadata registration fails.
1467    pub fn scale_real(&self, factor: f64) -> Result<TracedTensor> {
1468        let op = match self.dtype {
1469            DType::F64 => StdTensorOp::constant(factor),
1470            DType::F32 => StdTensorOp::constant(factor as f32),
1471            DType::I32 => StdTensorOp::constant(round_real_to_i32_for_op("scale_real", factor)?),
1472            DType::I64 => StdTensorOp::constant(round_real_to_i64(factor)?),
1473            DType::Bool => StdTensorOp::constant(bool_from_real_for_op("scale_real", factor)?),
1474            DType::C64 => StdTensorOp::constant(Complex64::new(factor, 0.0)),
1475            DType::C32 => StdTensorOp::constant(Complex32::new(factor as f32, 0.0)),
1476        };
1477        scale_with_constant(self, op)
1478    }
1479
1480    /// Scale by a complex scalar: `y = factor * x`.
1481    ///
1482    /// Only complex tensors support complex scaling. For a real scalar factor
1483    /// that should preserve the input dtype, prefer [`scale_real`](Self::scale_real).
1484    ///
1485    /// # Examples
1486    ///
1487    /// ```rust
1488    /// use num_complex::Complex64;
1489    /// # use tenferro_runtime::TracedTensor;
1490    /// # let x = TracedTensor::from_vec_col_major(
1491    /// #     vec![2],
1492    /// #     vec![Complex64::new(1.0, 0.0), Complex64::new(2.0, 0.0)],
1493    /// # )
1494    /// # .unwrap();
1495    /// let y = x.scale_complex(Complex64::new(0.0, 1.0)).unwrap(); // multiply by i
1496    /// ```
1497    ///
1498    /// # Errors
1499    ///
1500    /// Returns [`Error::Validation`] with `InvalidArgument` when a complex
1501    /// factor is applied to a non-complex dtype, or
1502    /// [`Error::RuntimeStateSource`] when output metadata registration fails.
1503    pub fn scale_complex(&self, factor: Complex64) -> Result<TracedTensor> {
1504        match self.dtype {
1505            DType::C64 => scale_with_constant(self, StdTensorOp::constant(factor)),
1506            DType::C32 => scale_with_constant(
1507                self,
1508                StdTensorOp::constant(Complex32::new(factor.re as f32, factor.im as f32)),
1509            ),
1510            DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool => {
1511                Err(graph_invalid_argument(
1512                    "scale_complex",
1513                    "dtype",
1514                    format!("requires complex tensor dtype, got {:?}", self.dtype),
1515                ))
1516            }
1517        }
1518    }
1519
1520    /// Elementwise exponential.
1521    ///
1522    /// # Examples
1523    ///
1524    /// ```rust
1525    /// # use tenferro_runtime::TracedTensor;
1526    /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1527    /// let y = x.exp().unwrap();
1528    /// ```
1529    ///
1530    /// # Errors
1531    ///
1532    /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1533    /// is unavailable or poisoned while recording the unary result.
1534    pub fn exp(&self) -> Result<TracedTensor> {
1535        self.apply_same_shape_unary(StdTensorOp::Exp)
1536    }
1537
1538    /// Elementwise natural logarithm.
1539    ///
1540    /// # Examples
1541    ///
1542    /// ```rust
1543    /// # use tenferro_runtime::TracedTensor;
1544    /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1545    /// let y = x.log().unwrap();
1546    /// ```
1547    ///
1548    /// # Errors
1549    ///
1550    /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1551    /// is unavailable or poisoned while recording the unary result.
1552    pub fn log(&self) -> Result<TracedTensor> {
1553        self.apply_same_shape_unary(StdTensorOp::Log)
1554    }
1555
1556    /// Elementwise sine.
1557    ///
1558    /// # Examples
1559    ///
1560    /// ```rust
1561    /// # use tenferro_runtime::TracedTensor;
1562    /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1563    /// let y = x.sin().unwrap();
1564    /// ```
1565    ///
1566    /// # Errors
1567    ///
1568    /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1569    /// is unavailable or poisoned while recording the unary result.
1570    pub fn sin(&self) -> Result<TracedTensor> {
1571        self.apply_same_shape_unary(StdTensorOp::Sin)
1572    }
1573
1574    /// Elementwise cosine.
1575    ///
1576    /// # Examples
1577    ///
1578    /// ```rust
1579    /// # use tenferro_runtime::TracedTensor;
1580    /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1581    /// let y = x.cos().unwrap();
1582    /// ```
1583    ///
1584    /// # Errors
1585    ///
1586    /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1587    /// is unavailable or poisoned while recording the unary result.
1588    pub fn cos(&self) -> Result<TracedTensor> {
1589        self.apply_same_shape_unary(StdTensorOp::Cos)
1590    }
1591
1592    /// Elementwise hyperbolic tangent.
1593    ///
1594    /// # Examples
1595    ///
1596    /// ```rust
1597    /// # use tenferro_runtime::TracedTensor;
1598    /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1599    /// let y = x.tanh().unwrap();
1600    /// ```
1601    ///
1602    /// # Errors
1603    ///
1604    /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1605    /// is unavailable or poisoned while recording the unary result.
1606    pub fn tanh(&self) -> Result<TracedTensor> {
1607        self.apply_same_shape_unary(StdTensorOp::Tanh)
1608    }
1609
1610    /// Elementwise square root.
1611    ///
1612    /// # Examples
1613    ///
1614    /// ```rust
1615    /// # use tenferro_runtime::TracedTensor;
1616    /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 4.0]).unwrap();
1617    /// let y = x.sqrt().unwrap();
1618    /// ```
1619    ///
1620    /// # Errors
1621    ///
1622    /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1623    /// is unavailable or poisoned while recording the unary result.
1624    pub fn sqrt(&self) -> Result<TracedTensor> {
1625        self.apply_same_shape_unary(StdTensorOp::Sqrt)
1626    }
1627
1628    /// Elementwise reciprocal square root.
1629    ///
1630    /// # Examples
1631    ///
1632    /// ```rust
1633    /// # use tenferro_runtime::TracedTensor;
1634    /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 4.0]).unwrap();
1635    /// let y = x.rsqrt().unwrap();
1636    /// ```
1637    ///
1638    /// # Errors
1639    ///
1640    /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1641    /// is unavailable or poisoned while recording the unary result.
1642    pub fn rsqrt(&self) -> Result<TracedTensor> {
1643        self.apply_same_shape_unary(StdTensorOp::Rsqrt)
1644    }
1645
1646    /// Elementwise power with NumPy-style broadcasting.
1647    ///
1648    /// # Examples
1649    ///
1650    /// ```rust
1651    /// # use tenferro_runtime::TracedTensor;
1652    /// # let base = TracedTensor::from_vec_col_major(vec![2], vec![2.0_f64, 3.0]).unwrap();
1653    /// # let exp = TracedTensor::from_vec_col_major(vec![2], vec![3.0_f64, 2.0]).unwrap();
1654    /// let y = base.pow(&exp);
1655    /// ```
1656    ///
1657    /// # Errors
1658    ///
1659    /// Returns [`Error::Validation`] with `ShapeMismatch` when the concrete
1660    /// operands cannot be broadcast, or [`Error::RuntimeStateSource`] when
1661    /// result metadata cannot be registered.
1662    ///
1663    /// # Deferred errors
1664    ///
1665    /// A symbolic broadcast mismatch or integer negative exponent is
1666    /// discovered at compile or execution and is returned as
1667    /// [`Error::TensorRuntime`] with a typed `ShapeMismatch` or
1668    /// `NegativeIntegerExponent` numerical source and the corresponding
1669    /// [`ErrorPhase`].
1670    pub fn pow(&self, other: &TracedTensor) -> Result<TracedTensor> {
1671        let (lhs, rhs) = broadcast_binary(self, other)?;
1672        apply_binary(
1673            StdTensorOp::Pow,
1674            &lhs,
1675            &rhs,
1676            lhs.rank,
1677            lhs.shape_hint.clone(),
1678        )
1679    }
1680
1681    /// Elementwise `exp(x) - 1`.
1682    ///
1683    /// # Examples
1684    ///
1685    /// ```rust
1686    /// # use tenferro_runtime::TracedTensor;
1687    /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1688    /// let y = x.expm1().unwrap();
1689    /// ```
1690    ///
1691    /// # Errors
1692    ///
1693    /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1694    /// is unavailable or poisoned while recording the unary result.
1695    pub fn expm1(&self) -> Result<TracedTensor> {
1696        self.apply_same_shape_unary(StdTensorOp::Expm1)
1697    }
1698
1699    /// Elementwise `log(1 + x)`.
1700    ///
1701    /// # Examples
1702    ///
1703    /// ```rust
1704    /// # use tenferro_runtime::TracedTensor;
1705    /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1706    /// let y = x.log1p().unwrap();
1707    /// ```
1708    ///
1709    /// # Errors
1710    ///
1711    /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1712    /// is unavailable or poisoned while recording the unary result.
1713    pub fn log1p(&self) -> Result<TracedTensor> {
1714        self.apply_same_shape_unary(StdTensorOp::Log1p)
1715    }
1716
1717    /// Convert the tensor to a different dtype using checked conversion.
1718    ///
1719    /// Use [`cast`](Self::cast) when a lossy dtype projection is intended.
1720    ///
1721    /// # Examples
1722    ///
1723    /// ```rust
1724    /// use tenferro_runtime::DType;
1725    /// # use tenferro_runtime::TracedTensor;
1726    /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1727    ///
1728    /// let y = x.convert(DType::C64)?;
1729    /// # Ok::<(), tenferro_runtime::Error>(())
1730    /// ```
1731    ///
1732    /// # Errors
1733    ///
1734    /// Returns [`tenferro_tensor::Error::UnsupportedDTypeConversion`] when the
1735    /// requested pair is outside tenferro's checked dtype-promotion lattice,
1736    /// or [`Error::Validation`] when graph metadata rejects the conversion.
1737    /// Use [`cast`](Self::cast) for explicit lossy dtype projection.
1738    pub fn convert(&self, to: DType) -> Result<TracedTensor> {
1739        tenferro_tensor::validate::validate_convert_dtype("TracedTensor::convert", self.dtype, to)?;
1740        self.cast(to)
1741    }
1742
1743    /// Cast the tensor to a different dtype using explicit dtype projection.
1744    ///
1745    /// `cast` may truncate, narrow precision, project complex values to their
1746    /// real component, or use boolean truthiness where the backend supports the
1747    /// requested projection.
1748    ///
1749    /// # Examples
1750    ///
1751    /// ```rust
1752    /// use tenferro_runtime::DType;
1753    /// # use tenferro_runtime::TracedTensor;
1754    /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.2_f64, -2.8]).unwrap();
1755    ///
1756    /// let y = x.cast(DType::I32).unwrap();
1757    /// ```
1758    ///
1759    /// # Errors
1760    ///
1761    /// Returns [`Error::TensorRuntime`] containing
1762    /// `UnsupportedDTypeConversion` when the requested input-to-target
1763    /// projection is not supported, or [`Error::RuntimeStateSource`] when
1764    /// converted-output metadata cannot be registered.
1765    pub fn cast(&self, to: DType) -> Result<TracedTensor> {
1766        if self.dtype == to {
1767            return Ok(self.clone());
1768        }
1769
1770        apply_unary_with_dtype(
1771            StdTensorOp::Convert {
1772                from: self.dtype,
1773                to,
1774            },
1775            self,
1776            self.rank,
1777            self.shape_hint.clone(),
1778            to,
1779        )
1780    }
1781
1782    /// Generalized tensor contraction.
1783    ///
1784    /// # Examples
1785    ///
1786    /// ```rust
1787    /// # use tenferro_runtime::{DotGeneralConfig, TracedTensor};
1788    /// # let a = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
1789    /// # let b = TracedTensor::from_vec_col_major(vec![3, 4], vec![1.0_f64; 12]).unwrap();
1790    /// # let config = DotGeneralConfig {
1791    /// #     lhs_contracting_dims: vec![1],
1792    /// #     rhs_contracting_dims: vec![0],
1793    /// #     lhs_batch_dims: vec![],
1794    /// #     rhs_batch_dims: vec![],
1795    /// # };
1796    /// let y = a.dot_general(&b, config)?;
1797    /// # Ok::<(), tenferro_runtime::Error>(())
1798    /// ```
1799    ///
1800    /// # Errors
1801    ///
1802    /// Returns [`Error::Validation`] with `RankMismatch`, `AxisOutOfBounds`,
1803    /// `DuplicateAxis`, or `AxisRoleConflict` when dimension numbers are
1804    /// invalid for the operand ranks, and [`Error::RuntimeStateSource`] when
1805    /// output metadata cannot be registered.
1806    ///
1807    /// # Deferred errors
1808    ///
1809    /// Contracting or batch dimensions whose sizes are symbolic are checked
1810    /// when concrete inputs reach compilation or execution. A mismatch is
1811    /// returned as [`Error::TensorRuntime`] with a typed `ShapeMismatch`
1812    /// source and its corresponding [`ErrorPhase`].
1813    pub fn dot_general(
1814        &self,
1815        other: &TracedTensor,
1816        config: DotGeneralConfig,
1817    ) -> Result<TracedTensor> {
1818        config
1819            .validate_dims_with_ranks(self.rank, other.rank)
1820            .map_err(|err| graph_tensor_error("dot_general", err))?;
1821        let lhs_free: Vec<usize> = (0..self.rank)
1822            .filter(|d| {
1823                !config.lhs_contracting_dims.contains(d) && !config.lhs_batch_dims.contains(d)
1824            })
1825            .collect();
1826        let rhs_free: Vec<usize> = (0..other.rank)
1827            .filter(|d| {
1828                !config.rhs_contracting_dims.contains(d) && !config.rhs_batch_dims.contains(d)
1829            })
1830            .collect();
1831        let out_rank = config.lhs_batch_dims.len() + lhs_free.len() + rhs_free.len();
1832        let out_shape_hint = match (&self.shape_hint, &other.shape_hint) {
1833            (Some(lhs_shape), Some(rhs_shape)) => {
1834                let mut out_shape = Vec::with_capacity(out_rank);
1835                for &d in &lhs_free {
1836                    out_shape.push(lhs_shape[d].clone());
1837                }
1838                for &d in &rhs_free {
1839                    out_shape.push(rhs_shape[d].clone());
1840                }
1841                for &d in &config.lhs_batch_dims {
1842                    out_shape.push(lhs_shape[d].clone());
1843                }
1844                Some(out_shape)
1845            }
1846            _ => None,
1847        };
1848
1849        apply_binary(
1850            StdTensorOp::DotGeneral { config },
1851            self,
1852            other,
1853            out_rank,
1854            out_shape_hint,
1855        )
1856    }
1857
1858    /// Matrix multiplication for rank-2 tensors.
1859    ///
1860    /// # Errors
1861    ///
1862    /// Returns [`Error::Validation`] with `RankMismatch` when either operand is
1863    /// not rank 2, `ShapeMismatch::ContractedDimensions` when known matrix
1864    /// dimensions differ, or [`Error::RuntimeStateSource`] when output
1865    /// metadata cannot be registered.
1866    ///
1867    /// # Deferred errors
1868    ///
1869    /// If either contracted dimension is symbolic, the mismatch is discovered
1870    /// at compilation or execution and returned as [`Error::TensorRuntime`]
1871    /// with its typed `ShapeMismatch` source.
1872    pub fn matmul(&self, other: &TracedTensor) -> Result<TracedTensor> {
1873        if self.rank != 2 {
1874            return Err(graph_validation(
1875                "TracedTensor::matmul",
1876                ValidationError::RankMismatch {
1877                    expected: 2,
1878                    actual: self.rank,
1879                },
1880            ));
1881        }
1882        if other.rank != 2 {
1883            return Err(graph_validation(
1884                "TracedTensor::matmul",
1885                ValidationError::RankMismatch {
1886                    expected: 2,
1887                    actual: other.rank,
1888                },
1889            ));
1890        }
1891        if let (Some(lhs_shape), Some(rhs_shape)) = (&self.shape_hint, &other.shape_hint) {
1892            if let (Some(lhs_cols), Some(rhs_rows)) =
1893                (lhs_shape[1].constant_value(), rhs_shape[0].constant_value())
1894            {
1895                if lhs_cols != rhs_rows {
1896                    return Err(graph_validation(
1897                        "TracedTensor::matmul",
1898                        ShapeMismatch::ContractedDimensions {
1899                            lhs_axis: 1,
1900                            lhs_size: lhs_cols,
1901                            rhs_axis: 0,
1902                            rhs_size: rhs_rows,
1903                        },
1904                    ));
1905                }
1906            }
1907        }
1908        self.dot_general(
1909            other,
1910            DotGeneralConfig {
1911                lhs_contracting_dims: vec![1],
1912                rhs_contracting_dims: vec![0],
1913                lhs_batch_dims: vec![],
1914                rhs_batch_dims: vec![],
1915            },
1916        )
1917    }
1918
1919    /// Sum over the given axes.
1920    ///
1921    /// # Examples
1922    ///
1923    /// ```rust
1924    /// # use tenferro_runtime::TracedTensor;
1925    /// # let x = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64; 4]).unwrap();
1926    /// let total = x.reduce_sum(None)?;
1927    /// let rows = x.reduce_sum(Some(&[1]))?;
1928    /// let identity = x.reduce_sum(Some(&[]))?;
1929    /// assert_eq!(total.rank, 0);
1930    /// assert_eq!(rows.rank, 1);
1931    /// assert_eq!(identity.rank, 2);
1932    /// # Ok::<(), tenferro_runtime::Error>(())
1933    /// ```
1934    ///
1935    /// # Errors
1936    ///
1937    /// Returns [`Error::Validation`] with `AxisOutOfBounds` when an axis is
1938    /// outside the input rank or `DuplicateAxis` when `axes` repeats an axis,
1939    /// or [`Error::RuntimeStateSource`] when output metadata cannot be
1940    /// registered.
1941    pub fn reduce_sum(&self, axes: Option<&[usize]>) -> Result<TracedTensor> {
1942        let axes = axes.map_or_else(|| (0..self.rank).collect(), <[usize]>::to_vec);
1943        let (out_rank, out_shape_hint) =
1944            reduction_output_meta(self, &axes, "TracedTensor::reduce_sum")?;
1945        apply_unary(
1946            StdTensorOp::ReduceSum { axes },
1947            self,
1948            out_rank,
1949            out_shape_hint,
1950        )
1951    }
1952
1953    /// Sum elementwise squares over the requested axes.
1954    ///
1955    /// Each value is squared in its input dtype before reduction. The initial
1956    /// supported dtypes are `f32` and `f64`; other dtypes return a typed
1957    /// unsupported error during execution. Passing an empty axis slice returns
1958    /// the elementwise square without reducing rank.
1959    ///
1960    /// This operation is useful when the squared sum is needed directly. Use
1961    /// the linalg norm APIs when a square root or complex magnitude semantics
1962    /// are required.
1963    ///
1964    /// # Errors
1965    ///
1966    /// Returns a typed validation error for invalid axes or a typed
1967    /// runtime-state error while registering output metadata.
1968    ///
1969    /// # Deferred errors
1970    ///
1971    /// Unsupported dtypes and backend execution failures are reported when the
1972    /// compiled graph is executed.
1973    /// # Examples
1974    ///
1975    /// ```rust
1976    /// # use tenferro_runtime::TracedTensor;
1977    /// # let x = TracedTensor::from_vec_col_major(
1978    /// #     vec![2, 2],
1979    /// #     vec![1.0_f64, 2.0, 3.0, 4.0],
1980    /// # )?;
1981    /// let squares = x.reduce_sum_squares(&[1])?;
1982    /// assert_eq!(squares.rank, 1);
1983    /// # Ok::<(), tenferro_runtime::Error>(())
1984    /// ```
1985    pub fn reduce_sum_squares(&self, axes: &[usize]) -> Result<TracedTensor> {
1986        let (out_rank, out_shape_hint) =
1987            reduction_output_meta(self, axes, "TracedTensor::reduce_sum_squares")?;
1988        apply_unary(
1989            StdTensorOp::ReduceSumSquares {
1990                axes: axes.to_vec(),
1991            },
1992            self,
1993            out_rank,
1994            out_shape_hint,
1995        )
1996    }
1997
1998    /// Reduce by taking the maximum along the given axes.
1999    ///
2000    /// Used by tropical (max-plus) compositions: a max-plus reduction over
2001    /// an axis is `ReduceMax` on that axis.
2002    ///
2003    /// # Examples
2004    ///
2005    /// ```rust
2006    /// # use tenferro_runtime::TracedTensor;
2007    /// # let x = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64; 4]).unwrap();
2008    /// let y = x.reduce_max(Some(&[0]))?;
2009    /// # Ok::<(), tenferro_runtime::Error>(())
2010    /// ```
2011    ///
2012    /// # Errors
2013    ///
2014    /// Returns [`Error::Validation`] with `AxisOutOfBounds` when an axis is
2015    /// outside the input rank or `DuplicateAxis` when `axes` repeats an axis,
2016    /// [`Error::Unsupported`] when a non-empty maximum reduction receives a
2017    /// complex dtype, or [`Error::RuntimeStateSource`] when output metadata
2018    /// cannot be registered.
2019    pub fn reduce_max(&self, axes: Option<&[usize]>) -> Result<TracedTensor> {
2020        let axes = axes.map_or_else(|| (0..self.rank).collect(), <[usize]>::to_vec);
2021        let (out_rank, out_shape_hint) =
2022            reduction_output_meta(self, &axes, "TracedTensor::reduce_max")?;
2023        try_apply_unary(
2024            StdTensorOp::ReduceMax { axes },
2025            self,
2026            out_rank,
2027            out_shape_hint,
2028            "TracedTensor::reduce_max",
2029        )
2030    }
2031
2032    /// Reduce by taking the minimum along the given axes.
2033    ///
2034    /// Used by tropical (min-plus) compositions: a min-plus reduction over
2035    /// an axis is `ReduceMin` on that axis.
2036    ///
2037    /// # Examples
2038    ///
2039    /// ```rust
2040    /// # use tenferro_runtime::TracedTensor;
2041    /// # let x = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64; 4]).unwrap();
2042    /// let y = x.reduce_min(Some(&[0]))?;
2043    /// # Ok::<(), tenferro_runtime::Error>(())
2044    /// ```
2045    ///
2046    /// # Errors
2047    ///
2048    /// Returns [`Error::Validation`] with `AxisOutOfBounds` when an axis is
2049    /// outside the input rank or `DuplicateAxis` when `axes` repeats an axis,
2050    /// [`Error::Unsupported`] when a non-empty minimum reduction receives a
2051    /// complex dtype, or [`Error::RuntimeStateSource`] when output metadata
2052    /// cannot be registered.
2053    pub fn reduce_min(&self, axes: Option<&[usize]>) -> Result<TracedTensor> {
2054        let axes = axes.map_or_else(|| (0..self.rank).collect(), <[usize]>::to_vec);
2055        let (out_rank, out_shape_hint) =
2056            reduction_output_meta(self, &axes, "TracedTensor::reduce_min")?;
2057        try_apply_unary(
2058            StdTensorOp::ReduceMin { axes },
2059            self,
2060            out_rank,
2061            out_shape_hint,
2062            "TracedTensor::reduce_min",
2063        )
2064    }
2065
2066    /// Reduce by taking the product along the given axes.
2067    ///
2068    /// # Examples
2069    ///
2070    /// ```rust
2071    /// # use tenferro_runtime::TracedTensor;
2072    /// # let x = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64; 4]).unwrap();
2073    /// let y = x.reduce_prod(Some(&[0]))?;
2074    /// # Ok::<(), tenferro_runtime::Error>(())
2075    /// ```
2076    ///
2077    /// # Errors
2078    ///
2079    /// Returns [`Error::Validation`] with `AxisOutOfBounds` when an axis is
2080    /// outside the input rank or `DuplicateAxis` when `axes` repeats an axis,
2081    /// or [`Error::RuntimeStateSource`] when output metadata cannot be
2082    /// registered.
2083    pub fn reduce_prod(&self, axes: Option<&[usize]>) -> Result<TracedTensor> {
2084        let axes = axes.map_or_else(|| (0..self.rank).collect(), <[usize]>::to_vec);
2085        let (out_rank, out_shape_hint) =
2086            reduction_output_meta(self, &axes, "TracedTensor::reduce_prod")?;
2087        apply_unary(
2088            StdTensorOp::ReduceProd { axes },
2089            self,
2090            out_rank,
2091            out_shape_hint,
2092        )
2093    }
2094
2095    /// Reshape without changing element order.
2096    ///
2097    /// # Examples
2098    ///
2099    /// ```rust
2100    /// # use tenferro_runtime::TracedTensor;
2101    /// # let x = TracedTensor::from_vec_col_major(vec![4], vec![1.0_f64; 4]).unwrap();
2102    /// let y = x.reshape(&[2, 2])?;
2103    /// # Ok::<(), tenferro_runtime::Error>(())
2104    /// ```
2105    ///
2106    /// # Errors
2107    ///
2108    /// Returns [`Error::Validation`] with `ShapeMismatch::ReshapeElementCount`
2109    /// when a concrete input has a different element count, or
2110    /// `IntegerOverflow` when the target shape product overflows `usize`.
2111    pub fn reshape(&self, shape: &[usize]) -> Result<TracedTensor> {
2112        validate_concrete_reshape_shape(self, shape)?;
2113        apply_unary_with_dtype(
2114            StdTensorOp::Reshape {
2115                to_shape: DimExpr::from_concrete(shape),
2116            },
2117            self,
2118            shape.len(),
2119            Some(shape.iter().copied().map(SymDim::from).collect()),
2120            self.dtype,
2121        )
2122    }
2123
2124    /// Return a symbolic expression for the size of one axis, suitable as
2125    /// an `InputDim`-style reference when composing with
2126    /// [`TracedTensor::reshape_sym`].
2127    ///
2128    /// Semantics: if this tensor's `shape_hint` has a symbolic
2129    /// (non-constant) entry for `axis`, that entry is returned
2130    /// verbatim. Otherwise — including when `shape_hint[axis]` is a
2131    /// concrete `SymDim::Concrete(n)` — a
2132    /// `SymDim::tensor_axis(self.id, axis)` reference is returned so the
2133    /// resulting graph remains shape-polymorphic if the same graph is
2134    /// later evaluated against a differently-shaped binding.
2135    ///
2136    /// For a canonical "what is the size of this axis?" query that
2137    /// reports the concrete size when it is known, prefer
2138    /// [`Self::axis_sym_dim`].
2139    ///
2140    /// # Examples
2141    ///
2142    /// ```rust
2143    /// # use tenferro_runtime::TracedTensor;
2144    /// # let x = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
2145    /// let rows = x.sym_size(0)?;
2146    /// let cols = x.sym_size(1)?;
2147    /// let y = x.reshape_sym(&[rows * cols]).unwrap();
2148    /// # Ok::<(), tenferro_runtime::Error>(())
2149    /// ```
2150    ///
2151    /// # Errors
2152    ///
2153    /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis` is
2154    /// outside this tensor's rank.
2155    pub fn sym_size(&self, axis: usize) -> Result<SymDim> {
2156        validate_traced_axis(self, axis, "TracedTensor::sym_size")?;
2157        Ok(self
2158            .shape_hint
2159            .as_ref()
2160            .and_then(|shape| shape.get(axis))
2161            .filter(|dim| dim.constant_value().is_none())
2162            .cloned()
2163            .unwrap_or_else(|| SymDim::tensor_axis(self.id, axis)))
2164    }
2165
2166    /// Return the canonical `SymDim` for `axis` — the concrete
2167    /// `SymDim::Concrete(n)` when the size is known, otherwise a symbolic
2168    /// expression identifying this tensor's axis.
2169    ///
2170    /// Unlike [`Self::sym_size`], this method does **not** rewrite
2171    /// concrete axes into `TensorAxis` references. It is the accessor
2172    /// external composition wrappers should use when building mixed
2173    /// concrete/symbolic target shapes for operations like
2174    /// [`Self::broadcast_in_dim_sym`].
2175    ///
2176    /// # Examples
2177    ///
2178    /// ```
2179    /// use tenferro_tensor::DType;
2180    /// use tenferro_runtime::TracedTensor;
2181    ///
2182    /// let a = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
2183    /// // Concrete axis: reports the constant size.
2184    /// assert_eq!(a.axis_sym_dim(0).unwrap().constant_value(), Some(2));
2185    ///
2186    /// let b = TracedTensor::input_symbolic_shape(DType::F64, 2).unwrap();
2187    /// // Fully symbolic leaf: reports a TensorAxis reference.
2188    /// assert!(b.axis_sym_dim(0).unwrap().constant_value().is_none());
2189    /// ```
2190    ///
2191    /// # Errors
2192    ///
2193    /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis` is
2194    /// outside this tensor's rank.
2195    pub fn axis_sym_dim(&self, axis: usize) -> Result<SymDim> {
2196        validate_traced_axis(self, axis, "TracedTensor::axis_sym_dim")?;
2197        match self.shape_hint.as_ref().and_then(|shape| shape.get(axis)) {
2198            Some(dim) => Ok(dim.clone()),
2199            None => Ok(SymDim::tensor_axis(self.id, axis)),
2200        }
2201    }
2202
2203    /// Return the full symbolic shape of this tensor when a `shape_hint`
2204    /// is present.
2205    ///
2206    /// Returns `None` for fully-symbolic placeholders produced via
2207    /// [`Self::input_symbolic_shape`] (where `shape_hint` is intentionally
2208    /// absent). For those, build the shape axis-by-axis via
2209    /// [`Self::axis_sym_dim`].
2210    ///
2211    /// # Examples
2212    ///
2213    /// ```
2214    /// use tenferro_tensor::DType;
2215    /// use tenferro_runtime::TracedTensor;
2216    ///
2217    /// let a = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
2218    /// assert!(a.sym_shape().is_some());
2219    /// assert_eq!(a.sym_shape().unwrap().len(), 2);
2220    ///
2221    /// let b = TracedTensor::input_symbolic_shape(DType::F64, 2).unwrap();
2222    /// assert!(b.sym_shape().is_none());
2223    /// ```
2224    pub fn sym_shape(&self) -> Option<&[SymDim]> {
2225        self.shape_hint.as_deref()
2226    }
2227
2228    /// Reshape using symbolic dimensions derived from traced tensor axes.
2229    ///
2230    /// # Examples
2231    ///
2232    /// ```rust
2233    /// # use tenferro_runtime::TracedTensor;
2234    /// # let x = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
2235    /// let rows = x.sym_size(0)?;
2236    /// let cols = x.sym_size(1)?;
2237    /// let y = x.reshape_sym(&[rows * cols]).unwrap();
2238    /// # Ok::<(), tenferro_runtime::Error>(())
2239    /// ```
2240    ///
2241    /// # Errors
2242    ///
2243    /// Returns [`Error::SymbolicShapeConversion`] when a supplied symbolic
2244    /// dimension cannot be mapped to this graph, or [`Error::RuntimeStateSource`]
2245    /// when result metadata cannot be registered.
2246    ///
2247    /// # Deferred errors
2248    ///
2249    /// Element-count compatibility for symbolic dimensions is checked when
2250    /// concrete inputs reach compilation or execution. A mismatch is returned
2251    /// as [`Error::TensorRuntime`] with a typed `ShapeMismatch` source.
2252    pub fn reshape_sym(&self, shape: &[SymDim]) -> Result<TracedTensor> {
2253        let tensor_map = [(self.id, 0usize)];
2254        let to_shape = shape
2255            .iter()
2256            .map(|dim| {
2257                dim.to_dim_expr(&tensor_map)
2258                    .map_err(|source| Error::SymbolicShapeConversion {
2259                        op: "TracedTensor::reshape_sym",
2260                        phase: ErrorPhase::GraphBuild,
2261                        source,
2262                    })
2263            })
2264            .collect::<Result<Vec<_>>>()?;
2265        let out_shape_hint = Some(shape.to_vec());
2266        apply_unary(
2267            StdTensorOp::Reshape { to_shape },
2268            self,
2269            shape.len(),
2270            out_shape_hint,
2271        )
2272    }
2273
2274    /// Broadcast into a larger shape with explicit dimension placement.
2275    ///
2276    /// # Examples
2277    ///
2278    /// ```rust
2279    /// # use tenferro_runtime::TracedTensor;
2280    /// # let x = TracedTensor::from_vec_col_major(vec![3], vec![1.0_f64; 3]).unwrap();
2281    /// let y = x.broadcast_in_dim(&[2, 3], &[1])?;
2282    /// # Ok::<(), tenferro_runtime::Error>(())
2283    /// ```
2284    ///
2285    /// # Errors
2286    ///
2287    /// Returns [`Error::Validation`] with `RankMismatch` when `dims` does not
2288    /// have one entry per input axis, `AxisOutOfBounds` or `DuplicateAxis` for
2289    /// an invalid output mapping, or `InvalidArgument` when known dimensions
2290    /// cannot broadcast. [`Error::RuntimeStateSource`] reports failure to
2291    /// register the result metadata.
2292    pub fn broadcast_in_dim(&self, shape: &[usize], dims: &[usize]) -> Result<TracedTensor> {
2293        let out_shape_hint: Vec<SymDim> = shape.iter().copied().map(SymDim::from).collect();
2294        validate_broadcast_in_dim_args(
2295            self,
2296            &out_shape_hint,
2297            dims,
2298            "TracedTensor::broadcast_in_dim",
2299        )?;
2300        apply_unary(
2301            StdTensorOp::BroadcastInDim {
2302                shape: DimExpr::from_concrete(shape),
2303                dims: dims.to_vec(),
2304            },
2305            self,
2306            shape.len(),
2307            Some(out_shape_hint),
2308        )
2309    }
2310
2311    /// Broadcast into a symbolic target shape with explicit dimension
2312    /// placement.
2313    ///
2314    /// Unlike [`Self::broadcast_in_dim`], each axis of `shape` is a
2315    /// [`SymDim`], so the target shape can mix concrete sizes (via
2316    /// `SymDim::from(n)`) with symbolic references to this tensor's axes
2317    /// (via [`Self::axis_sym_dim`]) or to axes of other traced tensors.
2318    ///
2319    /// When `shape` contains a `SymDim` that references a traced tensor
2320    /// other than `self`, the referenced tensor(s) must be supplied in
2321    /// `shape_refs`. They are wired into the built op as auxiliary
2322    /// shape-reference inputs — the op does not read their data, only
2323    /// their runtime shape. `shape_refs` must be listed in the same order
2324    /// in which their tensor IDs first appear when walking `shape` after
2325    /// any references to `self`. Usually the simplest correct thing is to
2326    /// pass each unique non-self reference tensor once.
2327    ///
2328    /// # Examples
2329    ///
2330    /// ```
2331    /// use tenferro_runtime::TracedTensor;
2332    ///
2333    /// let a = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
2334    /// let b = TracedTensor::from_vec_col_major(vec![3, 4], vec![1.0_f64; 12]).unwrap();
2335    /// let m = a.axis_sym_dim(0)?;
2336    /// let k = a.axis_sym_dim(1)?;
2337    /// let n = b.axis_sym_dim(1)?;
2338    /// // Broadcast `a[m, k]` to `[m, k, n]`, placing `a`'s axes at 0, 1
2339    /// // and taking `n` from `b` as an auxiliary shape reference.
2340    /// let a_b = a.broadcast_in_dim_sym(&[m, k, n], &[0, 1], &[&b])?;
2341    /// assert_eq!(a_b.rank, 3);
2342    /// # Ok::<(), tenferro_runtime::Error>(())
2343    /// ```
2344    ///
2345    /// # Errors
2346    ///
2347    /// Returns [`Error::Validation`] with `RankMismatch`, `AxisOutOfBounds`,
2348    /// `DuplicateAxis`, or `InvalidArgument` when the output mapping or shape
2349    /// references are invalid, [`Error::SymbolicShapeConversion`] for an
2350    /// unmappable symbolic dimension, or [`Error::RuntimeStateSource`] when
2351    /// metadata cannot be registered.
2352    ///
2353    /// # Deferred errors
2354    ///
2355    /// If a symbolic output dimension is smaller than a non-unit input axis,
2356    /// the concrete broadcast check is deferred to compilation or execution
2357    /// and is returned as [`Error::TensorRuntime`] with a typed validation
2358    /// source.
2359    pub fn broadcast_in_dim_sym(
2360        &self,
2361        shape: &[SymDim],
2362        dims: &[usize],
2363        shape_refs: &[&TracedTensor],
2364    ) -> Result<TracedTensor> {
2365        validate_broadcast_in_dim_args(self, shape, dims, "TracedTensor::broadcast_in_dim_sym")?;
2366
2367        // Build a dedup'd list of shape-reference tensors (first occurrence
2368        // wins) and index them starting at 1 — the primary input `self`
2369        // is at 0.
2370        let mut dedup_refs: Vec<&TracedTensor> = Vec::with_capacity(shape_refs.len());
2371        let mut tensor_map: Vec<(u64, usize)> = vec![(self.id, 0)];
2372        for &t in shape_refs {
2373            if !tensor_map.iter().any(|(id, _)| *id == t.id) {
2374                let idx = tensor_map.len();
2375                tensor_map.push((t.id, idx));
2376                dedup_refs.push(t);
2377            }
2378        }
2379
2380        let to_shape: Vec<DimExpr> = shape
2381            .iter()
2382            .map(|dim| {
2383                dim.to_dim_expr(&tensor_map)
2384                    .map_err(|source| Error::SymbolicShapeConversion {
2385                        op: "broadcast_in_dim_sym",
2386                        phase: ErrorPhase::GraphBuild,
2387                        source,
2388                    })
2389            })
2390            .collect::<Result<Vec<_>>>()?;
2391
2392        // Trim auxiliary shape-reference inputs down to those actually
2393        // used by the generated `DimExpr`s. If the target shape resolved
2394        // to all constants (the concrete-shape case) the op is a plain
2395        // unary broadcast with no extra parents. Otherwise the op needs
2396        // a contiguous prefix of shape-ref inputs covering every
2397        // referenced `input_idx`.
2398        let max_used_idx = DimExpr::max_input_idx_all(&to_shape).unwrap_or(0);
2399        let used_refs: Vec<&TracedTensor> = dedup_refs.into_iter().take(max_used_idx).collect();
2400
2401        let out_shape_hint = Some(shape.to_vec());
2402        apply_unary_with_shape_refs(
2403            StdTensorOp::BroadcastInDim {
2404                shape: to_shape,
2405                dims: dims.to_vec(),
2406            },
2407            self,
2408            &used_refs,
2409            shape.len(),
2410            out_shape_hint,
2411        )
2412    }
2413
2414    /// Slice with explicit start, limit, and stride per axis.
2415    ///
2416    /// # Errors
2417    ///
2418    /// Returns [`Error::Validation`] with `RankMismatch` when the start/limit/
2419    /// stride vectors do not match the input rank, `InvalidSliceStep` when a
2420    /// stride is zero, `InvalidSliceBounds` when a limit precedes its start,
2421    /// or [`Error::RuntimeStateSource`] when output metadata cannot be
2422    /// registered.
2423    pub fn slice(&self, config: SliceConfig) -> Result<TracedTensor> {
2424        let op = StdTensorOp::Slice(config);
2425        let (out_rank, out_shape_hint) =
2426            infer_traced_single_output_shape("TracedTensor::slice", &op, &[self])?;
2427        apply_unary(op, self, out_rank, out_shape_hint)
2428    }
2429
2430    /// Pad with zeros using StableHLO-style edge and interior padding.
2431    ///
2432    /// # Errors
2433    ///
2434    /// Returns [`Error::Validation`] with `RankMismatch` when padding vectors
2435    /// do not match the input rank, `InvalidArgument` for negative interior
2436    /// padding, or `IntegerOverflow` when the padded extent exceeds `usize`.
2437    /// [`Error::RuntimeStateSource`] is returned when output metadata cannot be
2438    /// registered.
2439    pub fn pad(&self, config: PadConfig) -> Result<TracedTensor> {
2440        let op = StdTensorOp::Pad(config);
2441        let (out_rank, out_shape_hint) =
2442            infer_traced_single_output_shape("TracedTensor::pad", &op, &[self])?;
2443        apply_unary(op, self, out_rank, out_shape_hint)
2444    }
2445
2446    /// Reverse the order of elements along the requested axes.
2447    ///
2448    /// # Errors
2449    ///
2450    /// Returns [`Error::Validation`] with `AxisOutOfBounds` when an axis is
2451    /// outside the input rank or `DuplicateAxis` when `axes` repeats one, or
2452    /// [`Error::RuntimeStateSource`] when result metadata cannot be
2453    /// registered.
2454    pub fn reverse(&self, axes: &[usize]) -> Result<TracedTensor> {
2455        validate_traced_axes(self.rank, axes, "TracedTensor::reverse")?;
2456        apply_unary(
2457            StdTensorOp::Reverse {
2458                axes: axes.to_vec(),
2459            },
2460            self,
2461            self.rank,
2462            self.shape_hint.clone(),
2463        )
2464    }
2465
2466    /// Gather slices from `self` using integer start indices.
2467    ///
2468    /// # Errors
2469    ///
2470    /// Returns [`Error::Validation`] with `RankMismatch`, `AxisOutOfBounds`,
2471    /// `DuplicateAxis`, or `ShapeMismatch` when indices or the gather
2472    /// configuration is incompatible with the input, and
2473    /// [`Error::RuntimeStateSource`] when output metadata cannot be
2474    /// registered.
2475    ///
2476    /// # Deferred errors
2477    ///
2478    /// Runtime index values are checked after binding. An out-of-range index
2479    /// is returned as [`Error::TensorRuntime`] with the backend's typed
2480    /// validation source and [`ErrorPhase::Execution`].
2481    pub fn gather(&self, indices: &TracedTensor, config: GatherConfig) -> Result<TracedTensor> {
2482        let op = StdTensorOp::Gather(config);
2483        let (out_rank, out_shape_hint) =
2484            infer_traced_single_output_shape("TracedTensor::gather", &op, &[self, indices])?;
2485        apply_binary_preserve_input_dtypes(op, self, indices, out_rank, out_shape_hint, self.dtype)
2486    }
2487
2488    /// Scatter updates into `self` using StableHLO scatter semantics.
2489    ///
2490    /// # Errors
2491    ///
2492    /// Returns [`Error::Validation`] with `RankMismatch`, `AxisOutOfBounds`,
2493    /// `DuplicateAxis`, or `ShapeMismatch` when indices, updates, or the
2494    /// scatter configuration is incompatible, [`Error::TensorRuntime`] with
2495    /// `UnsupportedDTypeConversion` when dtype promotion cannot be
2496    /// represented, or [`Error::RuntimeStateSource`] when output metadata
2497    /// cannot be registered.
2498    ///
2499    /// # Deferred errors
2500    ///
2501    /// Runtime index/update values are checked after binding. An invalid
2502    /// index or update shape is returned as [`Error::TensorRuntime`] with its
2503    /// typed validation source and [`ErrorPhase::Execution`].
2504    pub fn scatter(
2505        &self,
2506        indices: &TracedTensor,
2507        updates: &TracedTensor,
2508        config: ScatterConfig,
2509    ) -> Result<TracedTensor> {
2510        let op = StdTensorOp::Scatter(config);
2511        let (out_rank, out_shape_hint) = infer_traced_single_output_shape(
2512            "TracedTensor::scatter",
2513            &op,
2514            &[self, indices, updates],
2515        )?;
2516        let out_dtype = crate::shape_infer::promote_dtype(self.dtype, updates.dtype);
2517        let operand = if self.dtype != out_dtype {
2518            self.cast(out_dtype)?
2519        } else {
2520            self.clone()
2521        };
2522        let updates = if updates.dtype != out_dtype {
2523            updates.cast(out_dtype)?
2524        } else {
2525            updates.clone()
2526        };
2527        apply_ternary_with_output_dtype(
2528            op,
2529            &operand,
2530            indices,
2531            &updates,
2532            out_rank,
2533            out_shape_hint,
2534            out_dtype,
2535        )
2536    }
2537
2538    /// Slice using runtime start indices.
2539    ///
2540    /// # Errors
2541    ///
2542    /// Returns [`Error::Validation`] with `RankMismatch`, `AxisOutOfBounds`,
2543    /// or `InvalidArgument` when `starts` or `sizes` has an incompatible rank
2544    /// or extent, and [`Error::RuntimeStateSource`] when output metadata cannot
2545    /// be registered.
2546    ///
2547    /// # Deferred errors
2548    ///
2549    /// Runtime start values are checked after binding. An out-of-range start
2550    /// is returned as [`Error::TensorRuntime`] with the backend's typed
2551    /// validation source and [`ErrorPhase::Execution`].
2552    pub fn dynamic_slice(&self, starts: &TracedTensor, sizes: &[usize]) -> Result<TracedTensor> {
2553        let op = StdTensorOp::DynamicSlice {
2554            slice_sizes: sizes.to_vec(),
2555        };
2556        let (out_rank, out_shape_hint) =
2557            infer_traced_single_output_shape("TracedTensor::dynamic_slice", &op, &[self, starts])?;
2558        apply_binary_preserve_input_dtypes(op, self, starts, out_rank, out_shape_hint, self.dtype)
2559    }
2560
2561    /// Keep the lower triangle and zero the rest.
2562    ///
2563    /// # Examples
2564    ///
2565    /// ```rust
2566    /// # use tenferro_runtime::TracedTensor;
2567    /// let matrix = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64; 4])?;
2568    /// let lower = matrix.tril(0)?;
2569    /// assert_eq!(lower.rank, 2);
2570    /// # Ok::<(), tenferro_runtime::Error>(())
2571    /// ```
2572    ///
2573    /// # Errors
2574    ///
2575    /// Returns [`Error::RuntimeStateSource`] when traced output metadata
2576    /// registration is unavailable or inconsistent with the graph.
2577    pub fn tril(&self, k: i64) -> Result<TracedTensor> {
2578        apply_unary(
2579            StdTensorOp::Tril { k },
2580            self,
2581            self.rank,
2582            self.shape_hint.clone(),
2583        )
2584    }
2585
2586    /// Keep the upper triangle and zero the rest.
2587    ///
2588    /// # Examples
2589    ///
2590    /// ```rust
2591    /// # use tenferro_runtime::TracedTensor;
2592    /// let matrix = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64; 4])?;
2593    /// let upper = matrix.triu(0)?;
2594    /// assert_eq!(upper.rank, 2);
2595    /// # Ok::<(), tenferro_runtime::Error>(())
2596    /// ```
2597    ///
2598    /// # Errors
2599    ///
2600    /// Returns [`Error::RuntimeStateSource`] when traced output metadata
2601    /// registration is unavailable or inconsistent with the graph.
2602    pub fn triu(&self, k: i64) -> Result<TracedTensor> {
2603        apply_unary(
2604            StdTensorOp::Triu { k },
2605            self,
2606            self.rank,
2607            self.shape_hint.clone(),
2608        )
2609    }
2610
2611    /// Permute tensor axes.
2612    ///
2613    /// # Examples
2614    ///
2615    /// ```rust
2616    /// # use tenferro_runtime::TracedTensor;
2617    /// # let x = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
2618    /// let y = x.transpose(&[1, 0])?;
2619    /// # Ok::<(), tenferro_runtime::Error>(())
2620    /// ```
2621    ///
2622    /// # Errors
2623    ///
2624    /// Returns [`Error::Validation`] with `InvalidPermutationLength`,
2625    /// `AxisOutOfBounds`, or `DuplicateAxis` when `perm` is not a valid
2626    /// permutation of the tensor axes, or [`Error::RuntimeStateSource`] when
2627    /// output metadata registration fails.
2628    pub fn transpose(&self, perm: &[usize]) -> Result<TracedTensor> {
2629        validate_traced_perm(self.rank, perm, "TracedTensor::transpose")?;
2630        let out_shape_hint = self
2631            .shape_hint
2632            .as_ref()
2633            .map(|shape| perm.iter().map(|&p| shape[p].clone()).collect());
2634        apply_unary(
2635            StdTensorOp::Transpose {
2636                perm: perm.to_vec(),
2637            },
2638            self,
2639            self.rank,
2640            out_shape_hint,
2641        )
2642    }
2643
2644    /// Extract the diagonal along two axes.
2645    ///
2646    /// # Examples
2647    ///
2648    /// ```rust
2649    /// # use tenferro_runtime::TracedTensor;
2650    /// # let x = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64; 4]).unwrap();
2651    /// let y = x.extract_diag(0, 1)?;
2652    /// # Ok::<(), tenferro_runtime::Error>(())
2653    /// ```
2654    ///
2655    /// # Errors
2656    ///
2657    /// Returns [`Error::Validation`] with `AxisOutOfBounds` when either axis
2658    /// is outside the input rank or `InvalidArgument` when `axis_a == axis_b`.
2659    pub fn extract_diag(&self, axis_a: usize, axis_b: usize) -> Result<TracedTensor> {
2660        validate_traced_axis(self, axis_a, "TracedTensor::extract_diag")?;
2661        validate_traced_axis(self, axis_b, "TracedTensor::extract_diag")?;
2662        if axis_a == axis_b {
2663            return Err(graph_invalid_argument(
2664                "TracedTensor::extract_diag",
2665                "axes",
2666                "diagonal axes must be distinct",
2667            ));
2668        }
2669        let op = StdTensorOp::ExtractDiag { axis_a, axis_b };
2670        let (out_rank, out_shape_hint) =
2671            infer_traced_single_output_shape("TracedTensor::extract_diag", &op, &[self])?;
2672        apply_unary(op, self, out_rank, out_shape_hint)
2673    }
2674
2675    /// Embed a vector or lower-rank tensor along a diagonal.
2676    ///
2677    /// # Examples
2678    ///
2679    /// ```rust
2680    /// # use tenferro_runtime::TracedTensor;
2681    /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64; 2]).unwrap();
2682    /// let y = x.embed_diag(0, 1)?;
2683    /// # Ok::<(), tenferro_runtime::Error>(())
2684    /// ```
2685    ///
2686    /// # Errors
2687    ///
2688    /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis_a` is
2689    /// outside the input rank or `InvalidArgument` when `axis_b` is not a
2690    /// valid insertion axis.
2691    pub fn embed_diag(&self, axis_a: usize, axis_b: usize) -> Result<TracedTensor> {
2692        validate_traced_axis(self, axis_a, "TracedTensor::embed_diag")?;
2693        validate_traced_insert_axis(self.rank, axis_b, "TracedTensor::embed_diag")?;
2694        let out_shape_hint = self.shape_hint.as_ref().map(|shape| {
2695            let mut out_shape = shape.clone();
2696            out_shape.insert(axis_b, shape[axis_a].clone());
2697            out_shape
2698        });
2699        apply_unary(
2700            StdTensorOp::EmbedDiag { axis_a, axis_b },
2701            self,
2702            self.rank + 1,
2703            out_shape_hint,
2704        )
2705    }
2706
2707    /// Return the runtime size of one axis as a scalar `f64` tensor.
2708    ///
2709    /// The result is metadata-derived and therefore has no gradient.
2710    ///
2711    /// # Examples
2712    ///
2713    /// ```
2714    /// use tenferro_cpu::CpuBackend;
2715    /// use tenferro_runtime::{GraphCompiler, Runtime, TracedTensor};
2716    ///
2717    /// let x = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
2718    /// let cols = x.shape_of(1)?;
2719    /// let mut compiler = GraphCompiler::new();
2720    /// let program = compiler.compile(&cols).unwrap();
2721    /// let backend = CpuBackend::new();
2722    /// let mut builder = Runtime::builder();
2723    /// builder
2724    ///     .register_engine(tenferro_cpu::runtime_engine_registration(&backend).unwrap())
2725    ///     .unwrap();
2726    /// let runtime = builder.build().unwrap();
2727    /// let outputs = runtime.run_compiled(&program, &[]).unwrap();
2728    /// let out = &outputs[0];
2729    /// assert_eq!(out.shape(), &[] as &[usize]);
2730    /// # Ok::<(), tenferro_runtime::Error>(())
2731    /// ```
2732    ///
2733    /// # Errors
2734    ///
2735    /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis` is
2736    /// outside the input rank, or [`Error::RuntimeStateSource`] when scalar
2737    /// output metadata cannot be registered.
2738    pub fn shape_of(&self, axis: usize) -> Result<TracedTensor> {
2739        validate_traced_axis(self, axis, "TracedTensor::shape_of")?;
2740        apply_unary_with_dtype(
2741            StdTensorOp::ShapeOf { axis },
2742            self,
2743            0,
2744            Some(vec![]),
2745            DType::F64,
2746        )
2747    }
2748
2749    /// Truncate this tensor along `axis` to the first `size` elements.
2750    ///
2751    /// `size` is read at runtime from a scalar traced tensor. Values are
2752    /// rounded to the nearest integer, clamped to `[0, self.shape[axis]]`,
2753    /// and the output keeps the same element dtype as the input.
2754    ///
2755    /// # Examples
2756    ///
2757    /// ```
2758    /// use tenferro_cpu::CpuBackend;
2759    /// use tenferro_runtime::{GraphCompiler, Runtime, TracedTensor};
2760    ///
2761    /// let x = TracedTensor::from_vec_col_major(vec![4], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap();
2762    /// let size = TracedTensor::from_vec_col_major(vec![], vec![2.0_f64]).unwrap();
2763    /// let y = x.dynamic_truncate(&size, 0)?;
2764    /// let mut compiler = GraphCompiler::new();
2765    /// let program = compiler.compile(&y).unwrap();
2766    /// let backend = CpuBackend::new();
2767    /// let mut builder = Runtime::builder();
2768    /// builder
2769    ///     .register_engine(tenferro_cpu::runtime_engine_registration(&backend).unwrap())
2770    ///     .unwrap();
2771    /// let runtime = builder.build().unwrap();
2772    /// let outputs = runtime.run_compiled(&program, &[]).unwrap();
2773    /// let out = &outputs[0];
2774    /// assert_eq!(out.shape(), &[2]);
2775    /// # Ok::<(), tenferro_runtime::Error>(())
2776    /// ```
2777    ///
2778    /// # Errors
2779    ///
2780    /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis` is
2781    /// outside the input rank or `RankMismatch` when `size` is not scalar.
2782    ///
2783    /// # Deferred errors
2784    ///
2785    /// At execution, non-`f32`/`f64`/`i64` size dtypes return
2786    /// [`Error::TensorRuntime`] with `Unsupported`, non-finite size values
2787    /// return a typed `InvalidArgument`, and an empty scalar buffer returns a
2788    /// typed runtime-state source.
2789    pub fn dynamic_truncate(&self, size: &TracedTensor, axis: usize) -> Result<TracedTensor> {
2790        validate_traced_axis(self, axis, "TracedTensor::dynamic_truncate")?;
2791        if size.rank != 0 {
2792            return Err(graph_validation(
2793                "TracedTensor::dynamic_truncate",
2794                ValidationError::RankMismatch {
2795                    expected: 0,
2796                    actual: size.rank,
2797                },
2798            ));
2799        }
2800        apply_binary_preserve_input_dtypes(
2801            StdTensorOp::DynamicTruncate { axis },
2802            self,
2803            size,
2804            self.rank,
2805            None,
2806            self.dtype,
2807        )
2808    }
2809
2810    /// Pad this tensor with zeros along `axis` to match `reference.shape[axis]`.
2811    ///
2812    /// If `reference` is smaller along that axis, this is a no-op.
2813    ///
2814    /// # Examples
2815    ///
2816    /// ```
2817    /// use tenferro_cpu::CpuBackend;
2818    /// use tenferro_runtime::{GraphCompiler, Runtime, TracedTensor};
2819    ///
2820    /// let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
2821    /// let reference = TracedTensor::from_vec_col_major(vec![4], vec![0.0_f64, 0.0, 0.0, 0.0]).unwrap();
2822    /// let y = x.pad_to_match(&reference, 0)?;
2823    /// let mut compiler = GraphCompiler::new();
2824    /// let program = compiler.compile(&y).unwrap();
2825    /// let backend = CpuBackend::new();
2826    /// let mut builder = Runtime::builder();
2827    /// builder
2828    ///     .register_engine(tenferro_cpu::runtime_engine_registration(&backend).unwrap())
2829    ///     .unwrap();
2830    /// let runtime = builder.build().unwrap();
2831    /// let outputs = runtime.run_compiled(&program, &[]).unwrap();
2832    /// let out = &outputs[0];
2833    /// assert_eq!(out.shape(), &[4]);
2834    /// # Ok::<(), tenferro_runtime::Error>(())
2835    /// ```
2836    ///
2837    /// # Errors
2838    ///
2839    /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis` is
2840    /// outside either tensor's rank, or [`Error::RuntimeStateSource`] when
2841    /// output metadata cannot be registered.
2842    pub fn pad_to_match(&self, reference: &TracedTensor, axis: usize) -> Result<TracedTensor> {
2843        validate_traced_axis(self, axis, "TracedTensor::pad_to_match")?;
2844        validate_traced_axis(reference, axis, "TracedTensor::pad_to_match")?;
2845        let op = StdTensorOp::PadToMatch { axis };
2846        let (out_rank, out_shape_hint) = infer_traced_single_output_shape(
2847            "TracedTensor::pad_to_match",
2848            &op,
2849            &[self, reference],
2850        )?;
2851        apply_binary_preserve_input_dtypes(
2852            op,
2853            self,
2854            reference,
2855            out_rank,
2856            out_shape_hint,
2857            self.dtype,
2858        )
2859    }
2860}
2861
2862pub(crate) fn apply_unary(
2863    op: StdTensorOp,
2864    input: &TracedTensor,
2865    out_rank: usize,
2866    out_shape_hint: Option<Vec<SymDim>>,
2867) -> Result<TracedTensor> {
2868    let out_dtype = try_inferred_output_dtype(&op, &[input.dtype], "apply_unary")?;
2869    apply_unary_with_dtype(op, input, out_rank, out_shape_hint, out_dtype)
2870}
2871
2872fn try_apply_unary(
2873    op: StdTensorOp,
2874    input: &TracedTensor,
2875    out_rank: usize,
2876    out_shape_hint: Option<Vec<SymDim>>,
2877    context: &'static str,
2878) -> Result<TracedTensor> {
2879    let out_dtype = try_inferred_output_dtype(&op, &[input.dtype], context)?;
2880    apply_unary_with_dtype(op, input, out_rank, out_shape_hint, out_dtype)
2881}
2882
2883pub(crate) fn apply_unary_with_dtype(
2884    op: StdTensorOp,
2885    input: &TracedTensor,
2886    out_rank: usize,
2887    out_shape_hint: Option<Vec<SymDim>>,
2888    out_dtype: DType,
2889) -> Result<TracedTensor> {
2890    let mut builder = GraphBuilder::new();
2891    builder.add_parent(input.graph.clone());
2892    let input_ref = ValueRef::External(input.graph.values()[input.val].key.clone());
2893    let outputs = builder.add_operation(op, vec![input_ref], OperationRole::Primary);
2894    builder.set_outputs(outputs.clone());
2895    let graph = Arc::new(builder.build());
2896    let metadata_scope =
2897        register_single_output_metadata(graph.as_ref(), outputs[0], out_dtype, &out_shape_hint)?;
2898
2899    Ok(TracedTensor {
2900        id: next_traced_id(),
2901        rank: out_rank,
2902        dtype: out_dtype,
2903        graph,
2904        val: outputs[0],
2905        data: None,
2906        shape_hint: out_shape_hint,
2907        inputs_map: input.inputs_map.clone(),
2908        leaf_metas: input.leaf_metas.clone(),
2909        extra_roots: input.extra_roots.clone(),
2910        checkpoint_chain: input.checkpoint_chain.clone(),
2911        metadata_scopes: MetadataScopeChain::with_new(metadata_scope, [&input.metadata_scopes]),
2912        constraint_scopes: input.constraint_scopes.clone(),
2913    })
2914}
2915
2916/// Apply a unary-primary op that additionally references one or more
2917/// tensors for shape resolution only.
2918///
2919/// The primary `input` becomes op input 0; each tensor in `shape_refs`
2920/// becomes op input 1, 2, … in order. Used by
2921/// [`TracedTensor::broadcast_in_dim_sym`] when the target shape
2922/// references axes of tensors other than the primary input; the op
2923/// reads only their runtime shape, not their data.
2924pub(crate) fn apply_unary_with_shape_refs(
2925    op: StdTensorOp,
2926    input: &TracedTensor,
2927    shape_refs: &[&TracedTensor],
2928    out_rank: usize,
2929    out_shape_hint: Option<Vec<SymDim>>,
2930) -> Result<TracedTensor> {
2931    let mut builder = GraphBuilder::new();
2932    builder.add_parent(input.graph.clone());
2933    for t in shape_refs {
2934        builder.add_parent(t.graph.clone());
2935    }
2936    let mut op_inputs: Vec<ValueRef<StdTensorOp>> = Vec::with_capacity(1 + shape_refs.len());
2937    op_inputs.push(ValueRef::External(
2938        input.graph.values()[input.val].key.clone(),
2939    ));
2940    for t in shape_refs {
2941        op_inputs.push(ValueRef::External(t.graph.values()[t.val].key.clone()));
2942    }
2943    let outputs = builder.add_operation(op, op_inputs, OperationRole::Primary);
2944    builder.set_outputs(outputs.clone());
2945    let graph = Arc::new(builder.build());
2946    let metadata_scope =
2947        register_single_output_metadata(graph.as_ref(), outputs[0], input.dtype, &out_shape_hint)?;
2948
2949    let inputs_map =
2950        merge_traced_inputs_map(std::iter::once(input).chain(shape_refs.iter().copied()));
2951    let leaf_metas =
2952        merge_traced_leaf_metas(std::iter::once(input).chain(shape_refs.iter().copied()));
2953
2954    let mut extra_roots = input.extra_roots.clone();
2955    for t in shape_refs {
2956        extra_roots.extend(t.extra_roots.iter().cloned());
2957    }
2958
2959    let mut checkpoint_chain = input.checkpoint_chain.clone();
2960    for t in shape_refs {
2961        checkpoint_chain =
2962            CheckpointNode::merge_chains(checkpoint_chain, t.checkpoint_chain.clone());
2963    }
2964
2965    Ok(TracedTensor {
2966        id: next_traced_id(),
2967        rank: out_rank,
2968        dtype: input.dtype,
2969        graph,
2970        val: outputs[0],
2971        data: None,
2972        shape_hint: out_shape_hint,
2973        inputs_map,
2974        leaf_metas,
2975        extra_roots,
2976        checkpoint_chain,
2977        metadata_scopes: MetadataScopeChain::with_new(
2978            metadata_scope,
2979            std::iter::once(&input.metadata_scopes)
2980                .chain(shape_refs.iter().map(|tensor| &tensor.metadata_scopes)),
2981        ),
2982        constraint_scopes: ConstraintScopeChain::merge(
2983            std::iter::once(&input.constraint_scopes)
2984                .chain(shape_refs.iter().map(|tensor| &tensor.constraint_scopes)),
2985        ),
2986    })
2987}
2988
2989pub(crate) fn apply_nullary(
2990    op: StdTensorOp,
2991    rank: usize,
2992    dtype: DType,
2993    shape_hint: Option<Vec<SymDim>>,
2994) -> Result<TracedTensor> {
2995    let mut builder = GraphBuilder::new();
2996    let outputs = builder.add_operation(op, vec![], OperationRole::Primary);
2997    builder.set_outputs(outputs.clone());
2998    let graph = Arc::new(builder.build());
2999    let metadata_scope =
3000        register_single_output_metadata(graph.as_ref(), outputs[0], dtype, &shape_hint)?;
3001
3002    Ok(TracedTensor {
3003        id: next_traced_id(),
3004        rank,
3005        dtype,
3006        graph,
3007        val: outputs[0],
3008        data: None,
3009        shape_hint,
3010        inputs_map: Arc::new(HashMap::new()),
3011        leaf_metas: Arc::new(HashMap::new()),
3012        extra_roots: Vec::new(),
3013        checkpoint_chain: None,
3014        metadata_scopes: MetadataScopeChain::from_scope(metadata_scope),
3015        constraint_scopes: ConstraintScopeChain::empty(),
3016    })
3017}
3018
3019pub(crate) fn apply_binary(
3020    op: StdTensorOp,
3021    lhs: &TracedTensor,
3022    rhs: &TracedTensor,
3023    out_rank: usize,
3024    out_shape_hint: Option<Vec<SymDim>>,
3025) -> Result<TracedTensor> {
3026    let input_dtype = crate::shape_infer::promote_dtype_for_binary_op(&op, lhs.dtype, rhs.dtype);
3027    let out_dtype = try_inferred_output_dtype(&op, &[lhs.dtype, rhs.dtype], "apply_binary")?;
3028
3029    // Insert Convert ops when an input dtype differs from the primitive input dtype.
3030    let lhs = if lhs.dtype != input_dtype {
3031        lhs.cast(input_dtype)?
3032    } else {
3033        lhs.clone()
3034    };
3035    let rhs = if rhs.dtype != input_dtype {
3036        rhs.cast(input_dtype)?
3037    } else {
3038        rhs.clone()
3039    };
3040
3041    apply_binary_with_output_dtype(op, &lhs, &rhs, out_rank, out_shape_hint, out_dtype)
3042}
3043
3044fn try_apply_binary(
3045    op: StdTensorOp,
3046    lhs: &TracedTensor,
3047    rhs: &TracedTensor,
3048    out_rank: usize,
3049    out_shape_hint: Option<Vec<SymDim>>,
3050    context: &'static str,
3051) -> Result<TracedTensor> {
3052    let input_dtype = crate::shape_infer::promote_dtype_for_binary_op(&op, lhs.dtype, rhs.dtype);
3053    let out_dtype = try_inferred_output_dtype(&op, &[lhs.dtype, rhs.dtype], context)?;
3054
3055    let lhs = if lhs.dtype != input_dtype {
3056        lhs.cast(input_dtype)?
3057    } else {
3058        lhs.clone()
3059    };
3060    let rhs = if rhs.dtype != input_dtype {
3061        rhs.cast(input_dtype)?
3062    } else {
3063        rhs.clone()
3064    };
3065
3066    apply_binary_with_output_dtype(op, &lhs, &rhs, out_rank, out_shape_hint, out_dtype)
3067}
3068
3069pub(crate) fn apply_binary_preserve_input_dtypes(
3070    op: StdTensorOp,
3071    lhs: &TracedTensor,
3072    rhs: &TracedTensor,
3073    out_rank: usize,
3074    out_shape_hint: Option<Vec<SymDim>>,
3075    out_dtype: DType,
3076) -> Result<TracedTensor> {
3077    apply_binary_with_output_dtype(op, lhs, rhs, out_rank, out_shape_hint, out_dtype)
3078}
3079
3080pub(crate) fn apply_broadcast_binary_op(
3081    op: StdTensorOp,
3082    lhs: &TracedTensor,
3083    rhs: &TracedTensor,
3084) -> Result<TracedTensor> {
3085    let (lhs, rhs) = broadcast_binary(lhs, rhs)?;
3086    try_apply_binary(
3087        op,
3088        &lhs,
3089        &rhs,
3090        lhs.rank,
3091        lhs.shape_hint.clone(),
3092        "broadcast_binary",
3093    )
3094}
3095
3096pub(crate) fn apply_broadcast_ternary_op(
3097    op: StdTensorOp,
3098    first: &TracedTensor,
3099    second: &TracedTensor,
3100    third: &TracedTensor,
3101) -> Result<TracedTensor> {
3102    let (first, second, third) = broadcast_ternary(first, second, third)?;
3103    try_apply_ternary(
3104        op,
3105        &first,
3106        &second,
3107        &third,
3108        first.rank,
3109        first.shape_hint.clone(),
3110        "broadcast_ternary",
3111    )
3112}
3113
3114fn try_apply_ternary(
3115    op: StdTensorOp,
3116    first: &TracedTensor,
3117    second: &TracedTensor,
3118    third: &TracedTensor,
3119    out_rank: usize,
3120    out_shape_hint: Option<Vec<SymDim>>,
3121    context: &'static str,
3122) -> Result<TracedTensor> {
3123    let out_dtype =
3124        try_inferred_output_dtype(&op, &[first.dtype, second.dtype, third.dtype], context)?;
3125    let (first, second, third) = match op {
3126        StdTensorOp::Select => {
3127            let value_dtype = crate::shape_infer::promote_dtype(second.dtype, third.dtype);
3128            let second = if second.dtype != value_dtype {
3129                second.cast(value_dtype)?
3130            } else {
3131                second.clone()
3132            };
3133            let third = if third.dtype != value_dtype {
3134                third.cast(value_dtype)?
3135            } else {
3136                third.clone()
3137            };
3138            (first.clone(), second, third)
3139        }
3140        _ => {
3141            let input_dtype =
3142                crate::shape_infer::promote_dtypes([first.dtype, second.dtype, third.dtype]);
3143            let first = if first.dtype != input_dtype {
3144                first.cast(input_dtype)?
3145            } else {
3146                first.clone()
3147            };
3148            let second = if second.dtype != input_dtype {
3149                second.cast(input_dtype)?
3150            } else {
3151                second.clone()
3152            };
3153            let third = if third.dtype != input_dtype {
3154                third.cast(input_dtype)?
3155            } else {
3156                third.clone()
3157            };
3158            (first, second, third)
3159        }
3160    };
3161    apply_ternary_with_output_dtype(
3162        op,
3163        &first,
3164        &second,
3165        &third,
3166        out_rank,
3167        out_shape_hint,
3168        out_dtype,
3169    )
3170}
3171
3172fn apply_binary_with_output_dtype(
3173    op: StdTensorOp,
3174    lhs: &TracedTensor,
3175    rhs: &TracedTensor,
3176    out_rank: usize,
3177    out_shape_hint: Option<Vec<SymDim>>,
3178    out_dtype: DType,
3179) -> Result<TracedTensor> {
3180    let lhs_ref = ValueRef::External(lhs.graph.values()[lhs.val].key.clone());
3181    let rhs_ref = ValueRef::External(rhs.graph.values()[rhs.val].key.clone());
3182
3183    let mut builder = GraphBuilder::new();
3184    builder.add_parent(lhs.graph.clone());
3185    builder.add_parent(rhs.graph.clone());
3186    let outputs = builder.add_operation(op, vec![lhs_ref, rhs_ref], OperationRole::Primary);
3187    builder.set_outputs(outputs.clone());
3188    let graph = Arc::new(builder.build());
3189    let metadata_scope =
3190        register_single_output_metadata(graph.as_ref(), outputs[0], out_dtype, &out_shape_hint)?;
3191
3192    let mut extra_roots = lhs.extra_roots.clone();
3193    extra_roots.extend(rhs.extra_roots.iter().cloned());
3194
3195    Ok(TracedTensor {
3196        id: next_traced_id(),
3197        rank: out_rank,
3198        dtype: out_dtype,
3199        graph,
3200        val: outputs[0],
3201        data: None,
3202        shape_hint: out_shape_hint,
3203        inputs_map: merge_traced_inputs_map([lhs, rhs]),
3204        leaf_metas: merge_traced_leaf_metas([lhs, rhs]),
3205        extra_roots,
3206        checkpoint_chain: CheckpointNode::merge_chains(
3207            lhs.checkpoint_chain.clone(),
3208            rhs.checkpoint_chain.clone(),
3209        ),
3210        metadata_scopes: MetadataScopeChain::with_new(
3211            metadata_scope,
3212            [&lhs.metadata_scopes, &rhs.metadata_scopes],
3213        ),
3214        constraint_scopes: ConstraintScopeChain::merge([
3215            &lhs.constraint_scopes,
3216            &rhs.constraint_scopes,
3217        ]),
3218    })
3219}
3220
3221fn apply_ternary_with_output_dtype(
3222    op: StdTensorOp,
3223    first: &TracedTensor,
3224    second: &TracedTensor,
3225    third: &TracedTensor,
3226    out_rank: usize,
3227    out_shape_hint: Option<Vec<SymDim>>,
3228    out_dtype: DType,
3229) -> Result<TracedTensor> {
3230    let first_ref = ValueRef::External(first.graph.values()[first.val].key.clone());
3231    let second_ref = ValueRef::External(second.graph.values()[second.val].key.clone());
3232    let third_ref = ValueRef::External(third.graph.values()[third.val].key.clone());
3233
3234    let mut builder = GraphBuilder::new();
3235    builder.add_parent(first.graph.clone());
3236    builder.add_parent(second.graph.clone());
3237    builder.add_parent(third.graph.clone());
3238    let outputs = builder.add_operation(
3239        op,
3240        vec![first_ref, second_ref, third_ref],
3241        OperationRole::Primary,
3242    );
3243    builder.set_outputs(outputs.clone());
3244    let graph = Arc::new(builder.build());
3245    let metadata_scope =
3246        register_single_output_metadata(graph.as_ref(), outputs[0], out_dtype, &out_shape_hint)?;
3247
3248    let mut extra_roots = first.extra_roots.clone();
3249    extra_roots.extend(second.extra_roots.iter().cloned());
3250    extra_roots.extend(third.extra_roots.iter().cloned());
3251
3252    let checkpoint_chain = CheckpointNode::merge_chains(
3253        CheckpointNode::merge_chains(
3254            first.checkpoint_chain.clone(),
3255            second.checkpoint_chain.clone(),
3256        ),
3257        third.checkpoint_chain.clone(),
3258    );
3259
3260    Ok(TracedTensor {
3261        id: next_traced_id(),
3262        rank: out_rank,
3263        dtype: out_dtype,
3264        graph,
3265        val: outputs[0],
3266        data: None,
3267        shape_hint: out_shape_hint,
3268        inputs_map: merge_traced_inputs_map([first, second, third]),
3269        leaf_metas: merge_traced_leaf_metas([first, second, third]),
3270        extra_roots,
3271        checkpoint_chain,
3272        metadata_scopes: MetadataScopeChain::with_new(
3273            metadata_scope,
3274            [
3275                &first.metadata_scopes,
3276                &second.metadata_scopes,
3277                &third.metadata_scopes,
3278            ],
3279        ),
3280        constraint_scopes: ConstraintScopeChain::merge([
3281            &first.constraint_scopes,
3282            &second.constraint_scopes,
3283            &third.constraint_scopes,
3284        ]),
3285    })
3286}
3287
3288fn register_single_output_metadata(
3289    graph: &Graph<StdTensorOp>,
3290    output: LocalValueId,
3291    dtype: DType,
3292    shape_hint: &Option<Vec<SymDim>>,
3293) -> Result<GlobalMetadataScope> {
3294    if let Some(shape) = shape_hint {
3295        // Fresh graph output keys are generated in this builder, so metadata
3296        // registration failure would indicate a global metadata invariant bug.
3297        register_metadata_or_runtime_state(register_scoped_value_metadata(
3298            graph.values()[output].key.clone(),
3299            tensor_meta(dtype, shape.clone()),
3300        ))
3301    } else {
3302        // Fresh graph output keys are generated in this builder, so metadata
3303        // registration failure would indicate a global metadata invariant bug.
3304        register_metadata_or_runtime_state(register_scoped_graph_metadata(
3305            graph,
3306            std::iter::empty(),
3307        ))
3308    }
3309}
3310
3311impl TracedTensor {
3312    pub(crate) fn resolve_roots(&self) -> Vec<Arc<Graph<StdTensorOp>>> {
3313        let mut roots = Vec::with_capacity(1 + self.extra_roots.len());
3314        roots.push(self.graph.clone());
3315        roots.extend(self.extra_roots.iter().cloned());
3316        roots
3317    }
3318}
3319
3320#[cfg(test)]
3321mod tests;