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