Skip to main content

tenferro_runtime/
traced.rs

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