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