1use std::ops::Range;
2
3use tenferro_ops::std_tensor_op::StdTensorOp;
4use tenferro_tensor::{
5 GatherConfig, ShapeMismatch, SliceConfig, Tensor, TypedTensor, ValidationError,
6};
7
8use crate::error::{Error, ErrorPhase, Result};
9use crate::shape_infer::promote_dtypes;
10use crate::sym_dim::SymDim;
11use crate::traced::{
12 apply_binary_preserve_input_dtypes, infer_traced_single_output_shape, try_concrete_shape,
13};
14use crate::TracedTensor;
15
16fn normalize_existing_axis(op: &'static str, axis: isize, rank: usize) -> Result<usize> {
17 let normalized = if axis >= 0 {
18 axis as usize
19 } else {
20 rank.checked_sub(axis.unsigned_abs()).ok_or_else(|| {
21 Error::validation(
22 op,
23 ErrorPhase::GraphBuild,
24 ValidationError::AxisOutOfBounds {
25 axis: axis.unsigned_abs(),
26 rank,
27 },
28 )
29 })?
30 };
31 if normalized >= rank {
32 return Err(Error::validation(
33 op,
34 ErrorPhase::GraphBuild,
35 ValidationError::AxisOutOfBounds {
36 axis: axis.unsigned_abs(),
37 rank,
38 },
39 ));
40 }
41 Ok(normalized)
42}
43
44fn normalize_insert_axis(op: &'static str, axis: isize, rank: usize) -> Result<usize> {
45 let insert_rank = rank.checked_add(1).ok_or_else(|| {
46 Error::validation(
47 op,
48 ErrorPhase::GraphBuild,
49 ValidationError::AxisOutOfBounds {
50 axis: axis.unsigned_abs(),
51 rank,
52 },
53 )
54 })?;
55 let normalized = if axis >= 0 {
56 axis as usize
57 } else {
58 insert_rank
59 .checked_sub(axis.unsigned_abs())
60 .ok_or_else(|| {
61 Error::validation(
62 op,
63 ErrorPhase::GraphBuild,
64 ValidationError::AxisOutOfBounds {
65 axis: axis.unsigned_abs(),
66 rank: insert_rank,
67 },
68 )
69 })?
70 };
71 if normalized > rank {
72 return Err(Error::validation(
73 op,
74 ErrorPhase::GraphBuild,
75 ValidationError::AxisOutOfBounds {
76 axis: axis.unsigned_abs(),
77 rank: insert_rank,
78 },
79 ));
80 }
81 Ok(normalized)
82}
83
84fn index_select_config(
85 shape: &[usize],
86 axis: isize,
87 positions: &[usize],
88) -> Result<(Tensor, GatherConfig, Vec<usize>)> {
89 let axis = normalize_existing_axis("index_select", axis, shape.len())?;
90 let axis_extent = shape[axis];
91 for &position in positions {
92 if position >= axis_extent {
93 return Err(Error::validation(
94 "index_select",
95 ErrorPhase::GraphBuild,
96 ValidationError::InvalidArgument {
97 argument: "positions",
98 message: format!(
99 "position {position} out of bounds for axis {axis} with extent {axis_extent}"
100 ),
101 },
102 ));
103 }
104 }
105
106 let mut out_shape = shape.to_vec();
107 out_shape[axis] = positions.len();
108
109 let mut slice_sizes = shape.to_vec();
110 slice_sizes[axis] = 1;
111
112 let offset_dims = (0..shape.len()).filter(|&dim| dim != axis).collect();
113 let index_data = positions
114 .iter()
115 .map(|&position| {
116 i64::try_from(position).map_err(|_| {
117 Error::validation(
118 "index_select",
119 ErrorPhase::GraphBuild,
120 ValidationError::InvalidArgument {
121 argument: "positions",
122 message: format!("position {position} cannot be represented as i64"),
123 },
124 )
125 })
126 })
127 .collect::<Result<Vec<_>>>()?;
128 let indices = Tensor::from_typed::<i64>(TypedTensor::from_vec_col_major(
129 vec![positions.len(), 1],
130 index_data,
131 )?);
132
133 let config = GatherConfig {
134 offset_dims,
135 collapsed_slice_dims: vec![axis],
136 start_index_map: vec![axis],
137 index_vector_dim: 1,
138 slice_sizes,
139 };
140
141 Ok((indices, config, out_shape))
142}
143
144fn validate_stack_shapes(op: &'static str, shapes: &[&[usize]]) -> Result<()> {
145 let Some(first) = shapes.first() else {
146 return Err(Error::validation(
147 op,
148 ErrorPhase::GraphBuild,
149 ValidationError::InvalidArgument {
150 argument: "tensors",
151 message: "stack requires at least one input".into(),
152 },
153 ));
154 };
155 for shape in shapes.iter().skip(1) {
156 if *shape != *first {
157 return Err(Error::validation(
158 op,
159 ErrorPhase::GraphBuild,
160 ShapeMismatch::IncompatibleShapes {
161 lhs: first.iter().copied().collect(),
162 rhs: shape.iter().copied().collect(),
163 }
164 .into(),
165 ));
166 }
167 }
168 Ok(())
169}
170
171#[derive(Clone, Debug)]
172enum AxisSelection {
173 Slice {
174 axis: usize,
175 range: Range<usize>,
176 step: usize,
177 },
178 Take {
179 axis: usize,
180 indices: Vec<usize>,
181 },
182}
183
184fn concrete_shape_for_axis_slice(tensor: &TracedTensor, op: &'static str) -> Result<Vec<usize>> {
185 try_concrete_shape(tensor).ok_or_else(|| {
186 Error::validation(
187 op,
188 ErrorPhase::GraphBuild,
189 ValidationError::InvalidArgument {
190 argument: "shape",
191 message: format!("{op} requires a concrete shape hint"),
192 },
193 )
194 })
195}
196
197fn validate_axis_selection(
198 op: &'static str,
199 rank: usize,
200 seen: &mut [bool],
201 axis: usize,
202) -> Result<()> {
203 if axis >= rank {
204 return Err(Error::validation(
205 op,
206 ErrorPhase::GraphBuild,
207 ValidationError::AxisOutOfBounds { axis, rank },
208 ));
209 }
210 if seen[axis] {
211 return Err(Error::validation(
212 op,
213 ErrorPhase::GraphBuild,
214 ValidationError::DuplicateAxis {
215 axis,
216 role: "selection",
217 },
218 ));
219 }
220 seen[axis] = true;
221 Ok(())
222}
223
224fn apply_slice_axis_config(
225 op: &'static str,
226 shape: &[usize],
227 selections: &[AxisSelection],
228) -> Result<Option<SliceConfig>> {
229 let mut starts = vec![0; shape.len()];
230 let mut limits = shape.to_vec();
231 let mut strides = vec![1; shape.len()];
232 let mut has_slice = false;
233 for selection in selections {
234 let AxisSelection::Slice { axis, range, step } = selection else {
235 continue;
236 };
237 if *step == 0 {
238 return Err(Error::validation(
239 op,
240 ErrorPhase::GraphBuild,
241 ValidationError::InvalidSliceStep { step: 0 },
242 ));
243 }
244 let extent = shape[*axis];
245 if range.start > range.end || range.end > extent {
246 let start = isize::try_from(range.start).map_err(|_| {
247 Error::validation(op, ErrorPhase::GraphBuild, ValidationError::IntegerOverflow)
248 })?;
249 let end = isize::try_from(range.end).map_err(|_| {
250 Error::validation(op, ErrorPhase::GraphBuild, ValidationError::IntegerOverflow)
251 })?;
252 return Err(Error::validation(
253 op,
254 ErrorPhase::GraphBuild,
255 ValidationError::InvalidSliceBounds {
256 start,
257 end,
258 axis_len: extent,
259 },
260 ));
261 }
262 starts[*axis] = range.start;
263 limits[*axis] = range.end;
264 strides[*axis] = *step;
265 has_slice = true;
266 }
267 Ok(has_slice.then_some(SliceConfig {
268 starts,
269 limits,
270 strides,
271 }))
272}
273
274#[derive(Clone, Debug)]
291pub struct TracedSliceBuilder<'a> {
292 tensor: &'a TracedTensor,
293 selections: Vec<AxisSelection>,
294}
295
296impl<'a> TracedSliceBuilder<'a> {
297 fn new(tensor: &'a TracedTensor) -> Self {
298 Self {
299 tensor,
300 selections: Vec::new(),
301 }
302 }
303
304 pub fn axis(mut self, axis: usize, range: Range<usize>) -> Self {
318 self.selections.push(AxisSelection::Slice {
319 axis,
320 range,
321 step: 1,
322 });
323 self
324 }
325
326 pub fn axis_step(mut self, axis: usize, range: Range<usize>, step: usize) -> Self {
340 self.selections
341 .push(AxisSelection::Slice { axis, range, step });
342 self
343 }
344
345 pub fn take_axis(mut self, axis: usize, indices: &[usize]) -> Self {
359 self.selections.push(AxisSelection::Take {
360 axis,
361 indices: indices.to_vec(),
362 });
363 self
364 }
365
366 pub fn apply(self) -> Result<TracedTensor> {
387 let shape = concrete_shape_for_axis_slice(self.tensor, "slice_builder")?;
388 let mut seen = vec![false; shape.len()];
389 for selection in &self.selections {
390 let axis = match selection {
391 AxisSelection::Slice { axis, .. } | AxisSelection::Take { axis, .. } => *axis,
392 };
393 validate_axis_selection("slice_builder", shape.len(), &mut seen, axis)?;
394 }
395
396 let mut output = self.tensor.clone();
397 if let Some(config) = apply_slice_axis_config("slice_builder", &shape, &self.selections)? {
398 output = output.slice(config)?;
399 }
400 for selection in self.selections {
401 if let AxisSelection::Take { axis, indices } = selection {
402 output = output.take_axis(axis, &indices)?;
403 }
404 }
405 Ok(output)
406 }
407}
408
409impl TracedTensor {
410 pub fn slice_axis(&self, axis: usize, range: Range<usize>) -> Result<Self> {
430 self.slice_builder().axis(axis, range).apply()
431 }
432
433 pub fn slice_builder(&self) -> TracedSliceBuilder<'_> {
447 TracedSliceBuilder::new(self)
448 }
449
450 pub fn take_axis(&self, axis: usize, indices: &[usize]) -> Result<Self> {
470 let axis = isize::try_from(axis).map_err(|_| {
471 Error::validation(
472 "take_axis",
473 ErrorPhase::GraphBuild,
474 ValidationError::InvalidArgument {
475 argument: "axis",
476 message: format!("axis {axis} cannot be represented as isize"),
477 },
478 )
479 })?;
480 self.index_select(axis, indices)
481 }
482
483 pub fn index_select(&self, axis: isize, positions: &[usize]) -> Result<Self> {
527 let shape = try_concrete_shape(self).ok_or_else(|| {
528 Error::validation(
529 "index_select",
530 ErrorPhase::GraphBuild,
531 ValidationError::InvalidArgument {
532 argument: "shape",
533 message: "index_select requires a concrete shape hint".into(),
534 },
535 )
536 })?;
537 let (indices_tensor, config, out_shape) = index_select_config(&shape, axis, positions)?;
538 let indices = self.context().from_tensor_concrete_shape(indices_tensor)?;
539 apply_binary_preserve_input_dtypes(
540 StdTensorOp::Gather(config),
541 self,
542 &indices,
543 out_shape.len(),
544 Some(out_shape.into_iter().map(SymDim::from).collect()),
545 self.dtype,
546 )
547 }
548
549 pub fn stack(tensors: &[&Self], dim: isize) -> Result<Self> {
590 let first = tensors.first().copied().ok_or_else(|| {
591 Error::validation(
592 "stack",
593 ErrorPhase::GraphBuild,
594 ValidationError::InvalidArgument {
595 argument: "tensors",
596 message: "stack requires at least one input".into(),
597 },
598 )
599 })?;
600 let first_shape = try_concrete_shape(first).ok_or_else(|| {
601 Error::validation(
602 "stack",
603 ErrorPhase::GraphBuild,
604 ValidationError::InvalidArgument {
605 argument: "shape",
606 message: "stack requires concrete shape hints".into(),
607 },
608 )
609 })?;
610 let mut shapes = Vec::with_capacity(tensors.len());
611 shapes.push(first_shape.as_slice());
612 let mut owned_shapes = Vec::with_capacity(tensors.len().saturating_sub(1));
613 for tensor in tensors.iter().copied().skip(1) {
614 owned_shapes.push(try_concrete_shape(tensor).ok_or_else(|| {
615 Error::validation(
616 "stack",
617 ErrorPhase::GraphBuild,
618 ValidationError::InvalidArgument {
619 argument: "shape",
620 message: "stack requires concrete shape hints".into(),
621 },
622 )
623 })?);
624 }
625 shapes.extend(owned_shapes.iter().map(Vec::as_slice));
626 validate_stack_shapes("stack", &shapes)?;
627
628 let axis = normalize_insert_axis("stack", dim, first.rank)?;
629 let mut expanded_shape = first_shape;
630 expanded_shape.insert(axis, 1);
631 let mut out_shape = expanded_shape.clone();
632 out_shape[axis] = tensors.len();
633 let expanded = tensors
634 .iter()
635 .map(|tensor| tensor.reshape(&expanded_shape))
636 .collect::<Result<Vec<_>>>()?;
637 let refs = expanded.iter().collect::<Vec<_>>();
638 apply_nary_concatenate(
639 &refs,
640 axis,
641 out_shape.into_iter().map(SymDim::from).collect(),
642 )
643 }
644
645 pub fn concatenate(tensors: &[&Self], axis: usize) -> Result<Self> {
653 let first = tensors.first().copied().ok_or_else(|| {
654 Error::validation(
655 "concatenate",
656 ErrorPhase::GraphBuild,
657 ValidationError::InvalidArgument {
658 argument: "tensors",
659 message: "concatenate requires at least one input".into(),
660 },
661 )
662 })?;
663 if axis >= first.rank {
664 return Err(Error::validation(
665 "concatenate",
666 ErrorPhase::GraphBuild,
667 ValidationError::AxisOutOfBounds {
668 axis,
669 rank: first.rank,
670 },
671 ));
672 }
673 for tensor in tensors.iter().copied().skip(1) {
674 if tensor.rank != first.rank {
675 return Err(Error::validation(
676 "concatenate",
677 ErrorPhase::GraphBuild,
678 ValidationError::RankMismatch {
679 expected: first.rank,
680 actual: tensor.rank,
681 },
682 ));
683 }
684 }
685
686 let op = StdTensorOp::Concatenate {
687 axis,
688 input_count: tensors.len(),
689 };
690 let (_, out_shape_hint) =
691 infer_traced_single_output_shape("TracedTensor::concatenate", &op, tensors)?;
692 let out_shape = out_shape_hint.ok_or_else(|| {
693 Error::Internal("concatenate shape inference returned no shape hint".into())
694 })?;
695 apply_nary_concatenate(tensors, axis, out_shape)
696 }
697}
698
699fn apply_nary_concatenate(
700 tensors: &[&TracedTensor],
701 axis: usize,
702 out_shape: Vec<SymDim>,
703) -> Result<TracedTensor> {
704 let out_dtype = promote_dtypes(tensors.iter().map(|tensor| tensor.dtype));
705 let tensors = tensors
706 .iter()
707 .map(|tensor| {
708 if tensor.dtype != out_dtype {
709 tensor.cast(out_dtype)
710 } else {
711 Ok((*tensor).clone())
712 }
713 })
714 .collect::<Result<Vec<_>>>()?;
715
716 let Some(first) = tensors.first() else {
717 return Err(Error::invalid_argument(
718 "TracedSliceBuilder::concatenate",
719 ErrorPhase::GraphBuild,
720 "tensors",
721 "at least one tensor is required",
722 ));
723 };
724 for tensor in tensors.iter().skip(1) {
725 crate::traced::ensure_same_context(first, tensor)?;
726 }
727 let values: Vec<_> = tensors.iter().map(|tensor| tensor.value).collect();
728 let op = StdTensorOp::Concatenate {
729 axis,
730 input_count: tensors.len(),
731 };
732 let outputs = crate::trace_ops::append_core(&first.ctx, &op, &values)?;
733 crate::trace_ops::finish_value(
734 &first.ctx,
735 outputs[0],
736 out_shape.len(),
737 out_dtype,
738 Some(out_shape),
739 )
740}
741
742#[cfg(test)]
743mod tests;