Skip to main content

tenferro_runtime/
traced.rs

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