Skip to main content

tenferro_runtime/
traced.rs

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