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