Skip to main content

tenferro_runtime/
traced.rs

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