Skip to main content

candela/tensor/ops/
impl_op.rs

1#![allow(private_bounds)]
2use std::iter::zip;
3use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
4
5use crate::tensor::backend::Backend;
6use crate::tensor::errors::OpError;
7use crate::tensor::graph::NodeKind;
8use crate::tensor::mem_formats::layout::Layout;
9use crate::tensor::mem_formats::slice::SliceRange;
10use crate::tensor::ops::capabilities::{CanMatMul, FloatLike, NumericOp};
11use crate::tensor::ops::compute_layout;
12use crate::tensor::ops::def_op::{OpKind, OpKindScalar};
13use crate::tensor::skeleton::{
14    BakedPromise, BinaryResult, Clean, SkeletonPromise, SkeletonSlot, Tainting, UnaryResult,
15};
16use crate::tensor::traits::{Dimension, Numeric, Operand};
17use crate::tensor::{CachedTensorPromise, Tensor, TensorPromise};
18
19//////////////////////////////////////////////////////////////
20
21struct NodeWithLayout<T: Numeric, B: Backend> {
22    node: NodeKind<T, B>,
23    layout: Layout,
24}
25
26impl<T: Numeric, B: Backend> Dimension for NodeWithLayout<T, B> {
27    fn layout(&self) -> &Layout {
28        &self.layout
29    }
30}
31
32impl<T: Numeric, B: Backend> Operand<T, B> for NodeWithLayout<T, B> {
33    fn to_node(&self) -> NodeKind<T, B> {
34        self.node.clone()
35    }
36}
37
38impl<T: Numeric, B: Backend> Tainting for NodeWithLayout<T, B> {
39    type Mark = Clean;
40}
41
42//////////////////////////////////////////////////////////////
43
44// This works for checking broadcasting in a broad sense.
45// More specific check are done after the broadcast tries to happen.
46#[inline]
47fn find_broadcast_target(l1: &Layout, l2: &Layout) -> Vec<usize> {
48    let (largest, smallest) = if l1.shape().len() >= l2.shape().len() {
49        (l1, l2)
50    } else {
51        (l2, l1)
52    };
53    let largest_size = largest.shape().len();
54    debug_assert!(
55        largest.shape().len() >= smallest.shape().len(),
56        "broadcast helper precondition violated"
57    );
58    let diff = largest.shape().len() - smallest.shape().len();
59
60    let mut new_shape = vec![0_usize; largest_size];
61
62    for (i, (&dim1, &dim2)) in zip(l1.shape().iter().rev(), l2.shape().iter().rev()).enumerate() {
63        new_shape[largest_size - i - 1] = dim1.max(dim2);
64    }
65
66    new_shape[..diff].copy_from_slice(&largest.shape()[..diff]);
67
68    new_shape
69}
70
71#[inline]
72fn find_broadcast_target_until_batch(l1: &Layout, l2: &Layout) -> Option<(Vec<usize>, Vec<usize>)> {
73    let (largest, smallest) = if l1.shape().len() >= l2.shape().len() {
74        (l1, l2)
75    } else {
76        (l2, l1)
77    };
78    let largest_size = largest.shape().len();
79    debug_assert!(
80        largest.shape().len() >= smallest.shape().len(),
81        "matmul-batch broadcast helper precondition violated"
82    );
83    let diff = largest.shape().len() - smallest.shape().len();
84    let smallest_diff: usize = 2.min(smallest.shape().len());
85
86    if largest_size <= 2 {
87        return None;
88    }
89
90    let mut new_l1_shape = vec![0_usize; largest_size];
91    let mut new_l2_shape = vec![0_usize; largest_size];
92
93    for dim in 0..smallest_diff {
94        new_l1_shape[largest_size - dim - 1] = l1.shape()[l1.shape().len() - dim - 1];
95        new_l2_shape[largest_size - dim - 1] = l2.shape()[l2.shape().len() - dim - 1];
96    }
97
98    for (i, (&dim1, &dim2)) in zip(l1.shape().iter().rev(), l2.shape().iter().rev())
99        .enumerate()
100        .skip(smallest_diff)
101    {
102        let max = dim1.max(dim2);
103        new_l1_shape[largest_size - i - 1] = max;
104        new_l2_shape[largest_size - i - 1] = max;
105    }
106
107    for dim in 0..diff {
108        let n = largest.shape()[dim];
109        new_l1_shape[dim] = n;
110        new_l2_shape[dim] = n;
111    }
112
113    Some((new_l1_shape, new_l2_shape))
114}
115
116#[inline]
117fn is_blas_ready<T, B, D>(source: &D) -> bool
118where
119    B: Backend,
120    D: Operand<T, B>,
121{
122    let layout = source.layout();
123    if B::SUPPORTS_NON_CONTIGUOUS_MATMUL {
124        let last_axis = layout.stride().len() - 1;
125        return layout.stride()[last_axis] != 0 && layout.stride()[last_axis - 1] != 0;
126    }
127
128    // We don't need to check if it's contiguous because it will become a zero-copy or removed if not necessary
129    // by either the planner or the fusion system.
130    // layout.is_contiguous() ||
131    B::SUPPORTS_2D_TRANSPOSED_MATMUL && layout.is_last_axes_transposed()
132}
133
134type NodeTransform<Output, Backend> = Result<
135    (
136        NodeWithLayout<Output, Backend>,
137        NodeWithLayout<Output, Backend>,
138        Layout,
139    ),
140    OpError,
141>;
142
143#[inline]
144fn apply_transform_to_pair<T, B, D1, D2, F, N1, N2, L>(
145    lhs: &D1,
146    rhs: &D2,
147    filter: F,
148    transform_l: N1,
149    transform_r: N2,
150    compute_output_layout: L,
151) -> NodeTransform<T, B>
152where
153    T: Numeric,
154    B: Backend,
155    D1: Operand<T, B>,
156    D2: Operand<T, B>,
157    F: FnOnce(&D1, &D2) -> (bool, bool),
158    N1: FnOnce(&D1) -> Result<TensorPromise<T, B>, OpError>,
159    N2: FnOnce(&D2) -> Result<TensorPromise<T, B>, OpError>,
160    L: FnOnce(&Layout, &Layout) -> Result<Layout, OpError>,
161{
162    let (apply_l, apply_r) = filter(lhs, rhs);
163
164    let (node1, layout1, node2, layout2) = match (apply_l, apply_r) {
165        (false, false) => (
166            lhs.to_node(),
167            lhs.layout().clone(),
168            rhs.to_node(),
169            rhs.layout().clone(),
170        ),
171        (true, false) => {
172            let temp = transform_l(lhs)?;
173            let layout = temp.layout().clone();
174            (
175                NodeKind::Node(temp.graph),
176                layout,
177                rhs.to_node(),
178                rhs.layout().clone(),
179            )
180        }
181        (false, true) => {
182            let temp = transform_r(rhs)?;
183            let layout = temp.layout().clone();
184            (
185                lhs.to_node(),
186                lhs.layout().clone(),
187                NodeKind::Node(temp.graph),
188                layout,
189            )
190        }
191        (true, true) => {
192            let temp1 = transform_l(lhs)?;
193            let layout1 = temp1.layout().clone();
194            let temp2 = transform_r(rhs)?;
195            let layout2 = temp2.layout().clone();
196            (
197                NodeKind::Node(temp1.graph),
198                layout1,
199                NodeKind::Node(temp2.graph),
200                layout2,
201            )
202        }
203    };
204
205    let layout = compute_output_layout(&layout1, &layout2)?;
206    Ok((
207        NodeWithLayout {
208            node: node1,
209            layout: layout1,
210        },
211        NodeWithLayout {
212            node: node2,
213            layout: layout2,
214        },
215        layout,
216    ))
217}
218
219//////////////////////////////////////////////////////////////
220
221fn view_impl<T, B, D>(source: &D, shape: &[usize]) -> Result<TensorPromise<T, B>, OpError>
222where
223    T: Numeric,
224    B: Backend,
225    D: Operand<T, B>,
226{
227    let input = Box::new([source.to_node()]);
228    let layout = source.layout().view(shape)?;
229
230    Ok(TensorPromise::with_layout(
231        OpKind::View(layout.clone()),
232        input,
233        layout,
234    ))
235}
236
237fn broadcast_impl<T, B, D>(source: &D, shape: &[usize]) -> Result<TensorPromise<T, B>, OpError>
238where
239    T: Numeric,
240    B: Backend,
241    D: Operand<T, B>,
242{
243    let input = Box::new([source.to_node()]);
244    let layout = source.layout().broadcast(shape)?;
245
246    Ok(TensorPromise::with_layout(
247        OpKind::Broadcast(layout.clone()),
248        input,
249        layout,
250    ))
251}
252
253fn reshape_impl<T, B, D>(source: &D, shape: &[usize]) -> Result<TensorPromise<T, B>, OpError>
254where
255    T: Numeric,
256    B: Backend,
257    D: Operand<T, B>,
258{
259    let cont: TensorPromise<T, B> = as_contiguous_impl(source);
260    let layout = cont.graph.layout.view(shape)?;
261    let input = Box::new([NodeKind::Node(cont.graph)]);
262
263    Ok(TensorPromise::with_layout(
264        OpKind::View(layout.clone()),
265        input,
266        layout,
267    ))
268}
269
270fn slice_impl<T, B, D>(source: &D, range: &[SliceRange]) -> Result<TensorPromise<T, B>, OpError>
271where
272    T: Numeric,
273    B: Backend,
274    D: Operand<T, B>,
275{
276    let input = Box::new([source.to_node()]);
277    let layout = source.layout().slice(range)?;
278
279    Ok(TensorPromise::with_layout(
280        OpKind::Slice(layout.clone()),
281        input,
282        layout,
283    ))
284}
285
286fn transpose_impl<T, B, D>(source: &D) -> TensorPromise<T, B>
287where
288    T: Numeric,
289    B: Backend,
290    D: Operand<T, B>,
291{
292    let input = Box::new([source.to_node()]);
293
294    unsafe { TensorPromise::new(OpKind::Transpose, input).unwrap_unchecked() }
295}
296
297fn transpose_axes_impl<T, B, D>(source: &D, axes: &[usize]) -> Result<TensorPromise<T, B>, OpError>
298where
299    T: Numeric,
300    B: Backend,
301    D: Operand<T, B>,
302{
303    let input = Box::new([source.to_node()]);
304    let layout = source.layout().transpose_axes(axes)?;
305
306    Ok(TensorPromise::with_layout(
307        OpKind::TransposeAxes(layout.clone()),
308        input,
309        layout,
310    ))
311}
312
313fn as_contiguous_impl<T, B, D>(source: &D) -> TensorPromise<T, B>
314where
315    T: Numeric,
316    B: Backend,
317    D: Operand<T, B>,
318{
319    let node = source.to_node();
320    unsafe { TensorPromise::new(OpKind::AsContiguous, Box::new([node])).unwrap_unchecked() }
321}
322
323//////////////////////////////////////////////////////////////
324
325fn add_scalar_impl<T, B, D>(lhs: &D, rhs: T) -> TensorPromise<T, B>
326where
327    T: Numeric,
328    B: Backend,
329    D: Operand<T, B>,
330{
331    unsafe {
332        TensorPromise::new(
333            OpKind::ScalarOp(OpKindScalar::AxBy(T::MUL_NEUTRAL, rhs)),
334            Box::new([lhs.to_node()]),
335        )
336        .unwrap_unchecked()
337    }
338}
339
340fn sub_scalar_impl<T, B, D>(lhs: &D, rhs: T) -> TensorPromise<T, B>
341where
342    T: Numeric,
343    B: Backend,
344    D: Operand<T, B>,
345    T: Numeric + Neg<Output = T>,
346{
347    unsafe {
348        TensorPromise::new(
349            OpKind::ScalarOp(OpKindScalar::AxBy(T::MUL_NEUTRAL, -rhs)),
350            Box::new([lhs.to_node()]),
351        )
352        .unwrap_unchecked()
353    }
354}
355
356fn mul_scalar_impl<T, B, D>(lhs: &D, rhs: T) -> TensorPromise<T, B>
357where
358    T: Numeric,
359    B: Backend,
360    D: Operand<T, B>,
361{
362    unsafe {
363        TensorPromise::new(
364            OpKind::ScalarOp(OpKindScalar::AxBy(rhs, T::SUM_NEUTRAL)),
365            Box::new([lhs.to_node()]),
366        )
367        .unwrap_unchecked()
368    }
369}
370
371fn div_scalar_impl<T, B, D>(lhs: &D, rhs: T) -> TensorPromise<T, B>
372where
373    T: Numeric,
374    B: Backend,
375    D: Operand<T, B>,
376{
377    if rhs == T::SUM_NEUTRAL {
378        panic!("cannot divide by zero. stop.")
379    }
380
381    unsafe {
382        TensorPromise::new(
383            OpKind::ScalarOp(OpKindScalar::AxBy(T::MUL_NEUTRAL / rhs, T::SUM_NEUTRAL)),
384            Box::new([lhs.to_node()]),
385        )
386        .unwrap_unchecked()
387    }
388}
389
390fn exp_impl<T, B, D>(source: &D) -> TensorPromise<T, B>
391where
392    T: Numeric,
393    B: Backend,
394    D: Operand<T, B>,
395{
396    let input = Box::new([source.to_node()]);
397
398    unsafe { TensorPromise::new(OpKind::ScalarOp(OpKindScalar::Exp), input).unwrap_unchecked() }
399}
400
401fn ln_impl<T, B, D>(source: &D) -> TensorPromise<T, B>
402where
403    T: Numeric,
404    B: Backend,
405    D: Operand<T, B>,
406{
407    let input = Box::new([source.to_node()]);
408
409    unsafe { TensorPromise::new(OpKind::ScalarOp(OpKindScalar::Ln), input).unwrap_unchecked() }
410}
411
412fn log2_impl<T, B, D>(source: &D) -> TensorPromise<T, B>
413where
414    T: Numeric,
415    B: Backend,
416    D: Operand<T, B>,
417{
418    let input = Box::new([source.to_node()]);
419
420    unsafe { TensorPromise::new(OpKind::ScalarOp(OpKindScalar::Log2), input).unwrap_unchecked() }
421}
422
423fn relu_impl<T, B, D>(source: &D) -> TensorPromise<T, B>
424where
425    T: Numeric,
426    B: Backend,
427    D: Operand<T, B>,
428{
429    let input = Box::new([source.to_node()]);
430
431    unsafe { TensorPromise::new(OpKind::ScalarOp(OpKindScalar::ReLU), input).unwrap_unchecked() }
432}
433
434fn tanh_impl<T, B, D>(source: &D) -> TensorPromise<T, B>
435where
436    T: Numeric,
437    B: Backend,
438    D: Operand<T, B>,
439{
440    let input = Box::new([source.to_node()]);
441
442    unsafe { TensorPromise::new(OpKind::ScalarOp(OpKindScalar::Tanh), input).unwrap_unchecked() }
443}
444
445//////////////////////////////////////////////////////////////
446
447fn add_tensor_impl<T, B, D1, D2>(lhs: &D1, rhs: &D2) -> TensorPromise<T, B>
448where
449    T: Numeric,
450    B: Backend,
451    D1: Operand<T, B>,
452    D2: Operand<T, B>,
453{
454    let target = find_broadcast_target(lhs.layout(), rhs.layout());
455
456    let result = apply_transform_to_pair(
457        lhs,
458        rhs,
459        |l, r| (l.layout().shape() != target, r.layout().shape() != target),
460        |x| broadcast_impl(x, &target),
461        |x| broadcast_impl(x, &target),
462        |l1, l2| compute_layout(&OpKind::<T>::Add, &[l1, l2]),
463    );
464
465    if let Err(err) = result {
466        panic!("{}", err);
467    }
468
469    let (lhs_b, rhs_b, layout) = unsafe { result.unwrap_unchecked() };
470    TensorPromise::with_layout(
471        OpKind::Add,
472        [lhs_b.to_node(), rhs_b.to_node()].into(),
473        layout,
474    )
475}
476
477fn sub_tensor_impl<T, B, D1, D2>(lhs: &D1, rhs: &D2) -> TensorPromise<T, B>
478where
479    T: Numeric,
480    B: Backend,
481    D1: Operand<T, B>,
482    D2: Operand<T, B>,
483{
484    let target = find_broadcast_target(lhs.layout(), rhs.layout());
485
486    let result = apply_transform_to_pair(
487        lhs,
488        rhs,
489        |l, r| (l.layout().shape() != target, r.layout().shape() != target),
490        |x| broadcast_impl(x, &target),
491        |x| broadcast_impl(x, &target),
492        |l1, l2| compute_layout(&OpKind::<T>::Sub, &[l1, l2]),
493    );
494
495    if let Err(err) = result {
496        panic!("{}", err);
497    }
498
499    let (lhs_b, rhs_b, layout) = unsafe { result.unwrap_unchecked() };
500    TensorPromise::with_layout(
501        OpKind::Sub,
502        [lhs_b.to_node(), rhs_b.to_node()].into(),
503        layout,
504    )
505}
506
507fn mul_tensor_impl<T, B, D1, D2>(lhs: &D1, rhs: &D2) -> TensorPromise<T, B>
508where
509    T: Numeric,
510    B: Backend,
511    D1: Operand<T, B>,
512    D2: Operand<T, B>,
513{
514    let target = find_broadcast_target(lhs.layout(), rhs.layout());
515
516    let result = apply_transform_to_pair(
517        lhs,
518        rhs,
519        |l, r| (l.layout().shape() != target, r.layout().shape() != target),
520        |x| broadcast_impl(x, &target),
521        |x| broadcast_impl(x, &target),
522        |l1, l2| compute_layout(&OpKind::<T>::Mul, &[l1, l2]),
523    );
524
525    if let Err(err) = result {
526        panic!("{}", err);
527    }
528
529    let (lhs_b, rhs_b, layout) = unsafe { result.unwrap_unchecked() };
530    TensorPromise::with_layout(
531        OpKind::Mul,
532        [lhs_b.to_node(), rhs_b.to_node()].into(),
533        layout,
534    )
535}
536
537fn div_tensor_impl<T, B, D1, D2>(lhs: &D1, rhs: &D2) -> TensorPromise<T, B>
538where
539    T: Numeric,
540    B: Backend,
541    D1: Operand<T, B>,
542    D2: Operand<T, B>,
543{
544    let target = find_broadcast_target(lhs.layout(), rhs.layout());
545
546    let result = apply_transform_to_pair(
547        lhs,
548        rhs,
549        |l, r| (l.layout().shape() != target, r.layout().shape() != target),
550        |x| broadcast_impl(x, &target),
551        |x| broadcast_impl(x, &target),
552        |l1, l2| compute_layout(&OpKind::<T>::Div, &[l1, l2]),
553    );
554
555    if let Err(err) = result {
556        panic!("{}", err);
557    }
558
559    let (lhs_b, rhs_b, layout) = unsafe { result.unwrap_unchecked() };
560    TensorPromise::with_layout(
561        OpKind::Div,
562        [lhs_b.to_node(), rhs_b.to_node()].into(),
563        layout,
564    )
565}
566
567//////////////////////////////////////////////////////////////
568
569fn matmul_core<T, B, D1, D2>(lhs: &D1, rhs: &D2) -> Result<TensorPromise<T, B>, OpError>
570where
571    T: Numeric,
572    B: Backend,
573    D1: Operand<T, B>,
574    D2: Operand<T, B>,
575{
576    let (lhs_c, rhs_c, _) = apply_transform_to_pair(
577        lhs,
578        rhs,
579        |l, r| (!is_blas_ready(l), !is_blas_ready(r)),
580        |x| Ok(as_contiguous_impl(x)),
581        |x| Ok(as_contiguous_impl(x)),
582        |l1, _| Ok(l1.clone()),
583    )?;
584
585    let target = find_broadcast_target_until_batch(lhs_c.layout(), rhs_c.layout());
586
587    let (lhs_b, rhs_b, layout) = apply_transform_to_pair(
588        &lhs_c,
589        &rhs_c,
590        |l, r| {
591            (
592                target
593                    .as_ref()
594                    .is_some_and(|target| l.layout().shape() != target.0),
595                target
596                    .as_ref()
597                    .is_some_and(|target| r.layout().shape() != target.1),
598            )
599        },
600        |x| broadcast_impl(x, unsafe { &target.as_ref().unwrap_unchecked().0 }),
601        |x| broadcast_impl(x, unsafe { &target.as_ref().unwrap_unchecked().1 }),
602        |l1, l2| compute_layout(&OpKind::<T>::MatMul(T::MUL_NEUTRAL), &[l1, l2]),
603    )?;
604
605    Ok(TensorPromise::with_layout(
606        OpKind::MatMul(T::MUL_NEUTRAL),
607        [lhs_b.to_node(), rhs_b.to_node()].into(),
608        layout,
609    ))
610}
611
612// Drop the dim at position `len - 1 - from_end` via a metadata-only View.
613// Used by matmul's 1-D promotion to strip the size-1 dim introduced by
614// promoting a vector operand to a matrix.
615fn drop_dim_from_end<T, B, D>(source: &D, from_end: usize) -> Result<TensorPromise<T, B>, OpError>
616where
617    T: Numeric,
618    B: Backend,
619    D: Operand<T, B>,
620{
621    let mut new_shape: Vec<usize> = source.layout().shape().to_vec();
622    new_shape.remove(new_shape.len() - 1 - from_end);
623    view_impl(source, &new_shape)
624}
625
626fn matmul_tensor_impl<T, B, D1, D2>(lhs: &D1, rhs: &D2) -> Result<TensorPromise<T, B>, OpError>
627where
628    T: Numeric,
629    B: Backend,
630    D1: Operand<T, B>,
631    D2: Operand<T, B>,
632{
633    match (lhs.layout().shape().len(), rhs.layout().shape().len()) {
634        // [K] @ [K] -> [1, K] @ [K, 1] = [1, 1], strip to [1].
635        (1, 1) => {
636            let lhs_p = reshape_impl(lhs, &[1, lhs.layout().shape()[0]])?;
637            let rhs_p = reshape_impl(rhs, &[rhs.layout().shape()[0], 1])?;
638            let result = matmul_core(&lhs_p, &rhs_p)?;
639            drop_dim_from_end(&result, 0)
640        }
641        // [K] @ [..., K, N] -> [1, K] @ [..., K, N] = [..., 1, N], drop the prepended 1.
642        (1, _) => {
643            let lhs_p = reshape_impl(lhs, &[1, lhs.layout().shape()[0]])?;
644            let result = matmul_core(&lhs_p, rhs)?;
645            drop_dim_from_end(&result, 1)
646        }
647        // [..., M, K] @ [K] -> [..., M, K] @ [K, 1] = [..., M, 1], drop the appended 1.
648        (_, 1) => {
649            let rhs_p = reshape_impl(rhs, &[rhs.layout().shape()[0], 1])?;
650            let result = matmul_core(lhs, &rhs_p)?;
651            drop_dim_from_end(&result, 0)
652        }
653        // Both already >= 2-D (construction gate rules out 0-D): straight matmul.
654        _ => matmul_core(lhs, rhs),
655    }
656}
657
658//////////////////////////////////////////////////////////////
659
660fn sum_impl<T, B, D>(source: &D) -> TensorPromise<T, B>
661where
662    T: Numeric,
663    B: Backend,
664    D: Operand<T, B>,
665{
666    let input = Box::new([source.to_node()]);
667
668    unsafe { TensorPromise::new(OpKind::Sum, input).unwrap_unchecked() }
669}
670
671fn sum_axis_impl<T, B, D>(
672    source: &D,
673    axis: isize,
674    keep_dims: bool,
675) -> Result<TensorPromise<T, B>, OpError>
676where
677    T: Numeric,
678    B: Backend,
679    D: Operand<T, B>,
680{
681    let input = Box::new([source.to_node()]);
682    let op = OpKind::<T>::SumAxis(axis, keep_dims);
683    let layout = compute_layout(&op, &[source.layout()])?;
684
685    Ok(TensorPromise::with_layout(op, input, layout))
686}
687
688fn max_impl<T, B, D>(source: &D) -> TensorPromise<T, B>
689where
690    T: Numeric,
691    B: Backend,
692    D: Operand<T, B>,
693{
694    let input = Box::new([source.to_node()]);
695
696    unsafe { TensorPromise::new(OpKind::Max, input).unwrap_unchecked() }
697}
698
699fn max_axis_impl<T, B, D>(
700    source: &D,
701    axis: isize,
702    keep_dims: bool,
703) -> Result<TensorPromise<T, B>, OpError>
704where
705    T: Numeric,
706    B: Backend,
707    D: Operand<T, B>,
708{
709    let input = Box::new([source.to_node()]);
710    let op = OpKind::<T>::MaxAxis(axis, keep_dims);
711    let layout = compute_layout(&op, &[source.layout()])?;
712
713    Ok(TensorPromise::with_layout(op, input, layout))
714}
715
716fn mean_impl<T, B, D>(source: &D) -> TensorPromise<T, B>
717where
718    T: Numeric,
719    B: Backend,
720    D: Operand<T, B>,
721{
722    let input = Box::new([source.to_node()]);
723
724    unsafe { TensorPromise::new(OpKind::Mean, input).unwrap_unchecked() }
725}
726
727fn mean_axis_impl<T, B, D>(
728    source: &D,
729    axis: isize,
730    keep_dims: bool,
731) -> Result<TensorPromise<T, B>, OpError>
732where
733    T: Numeric,
734    B: Backend,
735    D: Operand<T, B>,
736{
737    let input = Box::new([source.to_node()]);
738    let op = OpKind::<T>::MeanAxis(axis, keep_dims);
739    let layout = compute_layout(&op, &[source.layout()])?;
740
741    Ok(TensorPromise::with_layout(op, input, layout))
742}
743
744//////////////////////////////////////////////////////////////
745
746macro_rules! impl_view {
747    ($ty:ident) => {
748        impl<T, B> $ty<T, B>
749        where
750            T: Numeric,
751            B: Backend,
752        {
753            /// Reinterprets the tensor's data as having shape `shape` without allocating.
754            ///
755            /// The tensor must be contiguous and must have the same length as the original tensor.
756            /// Use [`.reshape()`][Self::reshape] if the tensor may not be contiguous.
757            ///
758            /// # Examples
759            ///
760            /// ```
761            /// use candela::{Tensor, Dimension};
762            ///
763            /// let t = Tensor::from_slice(&[4.0, 3.0, 2.0, 1.0], &[4]);
764            /// // Shares the same underlying data as t
765            /// let v = t.view(&[2, 2]).unwrap().materialize();
766            ///
767            /// assert_eq!(v.shape(), &[2, 2]);
768            /// ```
769            ///
770            /// # Errors
771            ///
772            /// Returns [`OpError::NonContiguousView`] if the tensor is not contiguous or [`OpError::InvalidViewShape`] if the shape is invalid.
773            #[inline]
774            pub fn view(
775                &self,
776                shape: &[usize],
777            ) -> Result<<$ty<T, B> as UnaryResult<T, B>>::Output, OpError> {
778                view_impl(self, shape).map(<$ty<T, B> as UnaryResult<T, B>>::wrap)
779            }
780
781            /// Reinterprets the tensor's data as having shape `shape`,
782            /// allocating if the tensor is not contiguous.
783            ///
784            /// Unlike [`.view()`][Self::view], this never fails on a non-contiguous
785            /// tensor — it only requires that the new shape has the same total number
786            /// of elements as the original.
787            ///
788            /// # Examples
789            ///
790            /// ```
791            /// use candela::{Tensor, Dimension};
792            ///
793            /// let t = Tensor::from_slice(&[1.0, 2.0, 3.0, 4.0], &[2, 2]);
794            /// let r = t.transpose().reshape(&[4]).unwrap().materialize();
795            ///
796            /// assert_eq!(r.shape(), &[4]);
797            /// // r is contiguous as it was allocated in a new buffer.
798            /// assert!(r.is_contiguous());
799            /// ```
800            ///
801            /// # Errors
802            ///
803            /// Returns [`OpError::InvalidViewShape`] if `shape` does not have the same
804            /// total number of elements as the original.
805            #[inline]
806            pub fn reshape(
807                &self,
808                shape: &[usize],
809            ) -> Result<<$ty<T, B> as UnaryResult<T, B>>::Output, OpError> {
810                reshape_impl(self, shape).map(<$ty<T, B> as UnaryResult<T, B>>::wrap)
811            }
812        }
813    };
814}
815
816macro_rules! impl_slice {
817    ($ty:ident) => {
818        impl<T, B> $ty<T, B>
819        where
820            T: Numeric,
821            B: Backend,
822        {
823            /// Selects a rectangular subregion of the tensor, without allocating.
824            ///
825            /// Each [`SliceRange`] picks a range along one axis, applied from the
826            /// outermost axis inward; axes you leave out are kept whole. Build the
827            /// ranges with the [`s!`] macro using ordinary range syntax — negative
828            /// bounds count from the end. The result is a view into the original buffer.
829            ///
830            /// # Examples
831            ///
832            /// ```
833            /// use candela::{Tensor, Dimension, s};
834            ///
835            /// let t = Tensor::from_slice(&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0], &[2, 3]);
836            /// let sub = t.slice(s![1..2, 0..2]).unwrap().materialize(); // row 1, cols 0..2
837            ///
838            /// assert_eq!(sub.shape(), &[1, 2]);
839            /// ```
840            ///
841            /// # Errors
842            ///
843            /// Returns [`OpError::AxesOutOfBounds`] if more ranges are given than the
844            /// tensor has axes, or [`OpError::SliceOutOfBounds`] if a range is empty.
845            #[inline]
846            pub fn slice(
847                &self,
848                shape: &[SliceRange],
849            ) -> Result<<$ty<T, B> as UnaryResult<T, B>>::Output, OpError> {
850                slice_impl(self, shape).map(<$ty<T, B> as UnaryResult<T, B>>::wrap)
851            }
852        }
853    };
854}
855
856macro_rules! impl_transpose {
857    ($ty: ident) => {
858        impl<T, B> $ty<T, B>
859        where
860            T: Numeric,
861            B: Backend,
862        {
863            /// Reverses the order of every axis, without allocating.
864            ///
865            /// For a 2-D tensor this is the familiar matrix transpose; for higher
866            /// ranks it flips all axes at once (axis `i` becomes axis `rank - 1 - i`).
867            /// Only the layout changes — the data stays put until something forces a
868            /// copy, so reach for [`.as_contiguous()`][Self::as_contiguous] when you
869            /// need the transposed values in their own buffer. For an arbitrary
870            /// permutation, see [`.transpose_axes()`][Self::transpose_axes].
871            ///
872            /// # Examples
873            ///
874            /// ```
875            /// use candela::{Tensor, Dimension};
876            ///
877            /// let t = Tensor::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
878            /// let tt = t.transpose().materialize();
879            ///
880            /// assert_eq!(tt.shape(), &[3, 2]);
881            /// ```
882            #[inline]
883            pub fn transpose(&self) -> <$ty<T, B> as UnaryResult<T, B>>::Output {
884                <$ty<T, B> as UnaryResult<T, B>>::wrap(transpose_impl(self))
885            }
886        }
887    };
888}
889
890macro_rules! impl_transpose_axes {
891    ($ty:ident) => {
892        impl<T, B> $ty<T, B>
893        where
894            T: Numeric,
895            B: Backend,
896        {
897            /// Reorders the axes by an explicit permutation, without allocating.
898            ///
899            /// `axes` must list every axis index exactly once: `transpose_axes(&[1, 0])`
900            /// is the plain 2-D [`.transpose()`][Self::transpose], while `&[0, 2, 1]`
901            /// swaps only the last two axes of a rank-3 tensor and leaves the first
902            /// alone. Like [`.transpose()`][Self::transpose] it only relabels the
903            /// layout — see [`.as_contiguous()`][Self::as_contiguous] to materialize
904            /// the reordered values.
905            ///
906            /// # Examples
907            ///
908            /// ```
909            /// use candela::{Tensor, Dimension};
910            ///
911            /// let t = Tensor::from_slice(&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0], &[1, 2, 3]);
912            /// let s = t.transpose_axes(&[0, 2, 1]).unwrap().materialize();
913            ///
914            /// assert_eq!(s.shape(), &[1, 3, 2]);
915            /// ```
916            ///
917            /// # Errors
918            ///
919            /// Returns [`OpError::NotEnoughAxes`] if `axes` doesn't have one entry per
920            /// axis, or [`OpError::AxesOutOfBounds`] if an index is out of range or
921            /// repeated (so the list isn't a valid permutation).
922            #[inline]
923            pub fn transpose_axes(
924                &self,
925                axes: &[usize],
926            ) -> Result<<$ty<T, B> as UnaryResult<T, B>>::Output, OpError> {
927                transpose_axes_impl(self, axes).map(<$ty<T, B> as UnaryResult<T, B>>::wrap)
928            }
929        }
930    };
931}
932
933macro_rules! impl_as_contiguous {
934    ($ty: ident) => {
935        impl<T, B> $ty<T, B>
936        where
937            T: Numeric,
938            B: Backend,
939        {
940            /// Packs the tensor into a fresh contiguous buffer in row-major order.
941            ///
942            /// Layout-only ops like [`.transpose()`][Self::transpose] and
943            /// [`.slice()`][Self::slice] leave the data where it is and only change
944            /// how it's addressed. `as_contiguous` turns such a view back into a
945            /// densely laid-out tensor. If the input is already contiguous it costs
946            /// nothing — the call collapses to a no-op. Candela also inserts it
947            /// automatically wherever an op needs contiguous memory (a BLAS matmul, a
948            /// [`.reshape()`][Self::reshape]), so you rarely call it by hand.
949            ///
950            /// # Examples
951            ///
952            /// ```
953            /// use candela::{Tensor, Dimension};
954            ///
955            /// let t = Tensor::from_slice(&[1.0, 2.0, 3.0, 4.0], &[2, 2]);
956            /// // transpose is a view; as_contiguous lays the transposed values out for real.
957            /// let c = t.transpose().as_contiguous().materialize();
958            ///
959            /// assert!(c.is_contiguous());
960            /// assert_eq!(c.data(), &[1.0, 3.0, 2.0, 4.0]);
961            /// ```
962            #[inline]
963            pub fn as_contiguous(&self) -> <$ty<T, B> as UnaryResult<T, B>>::Output {
964                <$ty<T, B> as UnaryResult<T, B>>::wrap(as_contiguous_impl(self))
965            }
966        }
967    };
968}
969
970macro_rules! impl_broadcast {
971    ($ty:ident) => {
972        impl<T, B> $ty<T, B>
973        where
974            T: Numeric,
975            B: Backend,
976        {
977            /// Expands the tensor to a larger shape by repeating elements along new
978            /// or size-1 axes, without allocating.
979            ///
980            /// Broadcasting follows NumPy's right-aligned rules: a target axis must
981            /// either match the source or expand from size 1, and extra leading axes
982            /// are added on the left. No data is copied — the repeated axes are faked
983            /// with zero strides. The arithmetic operators broadcast on their own, so
984            /// you mostly need this only to force a specific shape up front.
985            ///
986            /// # Examples
987            ///
988            /// ```
989            /// use candela::{Tensor, Dimension};
990            ///
991            /// let row = Tensor::from_slice(&[1.0, 2.0, 3.0], &[1, 3]);
992            /// let b = row.broadcast(&[2, 3]).unwrap().materialize();
993            ///
994            /// assert_eq!(b.shape(), &[2, 3]);
995            /// ```
996            ///
997            /// # Errors
998            ///
999            /// Returns [`OpError::CannotBroadcast`] if the target shape has fewer axes
1000            /// than the source, or an axis is neither equal to the source nor
1001            /// expandable from 1.
1002            #[inline]
1003            pub fn broadcast(
1004                &self,
1005                shape: &[usize],
1006            ) -> Result<<$ty<T, B> as UnaryResult<T, B>>::Output, OpError> {
1007                broadcast_impl(self, shape).map(<$ty<T, B> as UnaryResult<T, B>>::wrap)
1008            }
1009        }
1010    };
1011}
1012
1013macro_rules! impl_reshape_like {
1014    ($ty:ident) => {
1015        impl_view!($ty);
1016        impl_slice!($ty);
1017        impl_transpose!($ty);
1018        impl_transpose_axes!($ty);
1019        impl_as_contiguous!($ty);
1020        impl_broadcast!($ty);
1021    };
1022}
1023//////////////////////////////////////////////////////////////
1024
1025macro_rules! impl_add_scalar {
1026    ($ty:ident) => {
1027        impl<T, B> Add<T> for &$ty<T, B>
1028        where
1029            T: NumericOp,
1030            B: Backend,
1031        {
1032            type Output = <$ty<T, B> as UnaryResult<T, B>>::Output;
1033
1034            #[inline]
1035            fn add(self, rhs: T) -> Self::Output {
1036                <$ty<T, B> as UnaryResult<T, B>>::wrap(add_scalar_impl(self, rhs))
1037            }
1038        }
1039
1040        impl<T, B> Add<T> for $ty<T, B>
1041        where
1042            T: NumericOp,
1043            B: Backend,
1044        {
1045            type Output = <$ty<T, B> as UnaryResult<T, B>>::Output;
1046
1047            #[inline]
1048            fn add(self, rhs: T) -> Self::Output {
1049                (&self).add(rhs)
1050            }
1051        }
1052    };
1053}
1054
1055macro_rules! impl_sub_scalar {
1056    ($ty:ident) => {
1057        impl<T, B> Sub<T> for &$ty<T, B>
1058        where
1059            T: NumericOp + Neg<Output = T>,
1060            B: Backend,
1061        {
1062            type Output = <$ty<T, B> as UnaryResult<T, B>>::Output;
1063
1064            #[inline]
1065            fn sub(self, rhs: T) -> Self::Output {
1066                <$ty<T, B> as UnaryResult<T, B>>::wrap(sub_scalar_impl(self, rhs))
1067            }
1068        }
1069
1070        impl<T, B> Sub<T> for $ty<T, B>
1071        where
1072            T: NumericOp + Neg<Output = T>,
1073            B: Backend,
1074        {
1075            type Output = <$ty<T, B> as UnaryResult<T, B>>::Output;
1076
1077            #[inline]
1078            fn sub(self, rhs: T) -> Self::Output {
1079                (&self).sub(rhs)
1080            }
1081        }
1082    };
1083}
1084
1085macro_rules! impl_mul_scalar {
1086    ($ty:ident) => {
1087        impl<T, B> Mul<T> for &$ty<T, B>
1088        where
1089            T: NumericOp,
1090            B: Backend,
1091        {
1092            type Output = <$ty<T, B> as UnaryResult<T, B>>::Output;
1093
1094            #[inline]
1095            fn mul(self, rhs: T) -> Self::Output {
1096                <$ty<T, B> as UnaryResult<T, B>>::wrap(mul_scalar_impl(self, rhs))
1097            }
1098        }
1099
1100        impl<T, B> Mul<T> for $ty<T, B>
1101        where
1102            T: NumericOp,
1103            B: Backend,
1104        {
1105            type Output = <$ty<T, B> as UnaryResult<T, B>>::Output;
1106
1107            #[inline]
1108            fn mul(self, rhs: T) -> Self::Output {
1109                (&self).mul(rhs)
1110            }
1111        }
1112    };
1113}
1114
1115macro_rules! impl_div_scalar {
1116    ($ty:ident) => {
1117        impl<T, B> Div<T> for &$ty<T, B>
1118        where
1119            T: NumericOp,
1120            B: Backend,
1121        {
1122            type Output = <$ty<T, B> as UnaryResult<T, B>>::Output;
1123
1124            /// # Panics
1125            ///
1126            /// Panics when the operator is applied — not at `.materialize()` — if
1127            /// `rhs` is zero.
1128            #[inline]
1129            fn div(self, rhs: T) -> Self::Output {
1130                <$ty<T, B> as UnaryResult<T, B>>::wrap(div_scalar_impl(self, rhs))
1131            }
1132        }
1133
1134        impl<T, B> Div<T> for $ty<T, B>
1135        where
1136            T: NumericOp,
1137            B: Backend,
1138        {
1139            type Output = <$ty<T, B> as UnaryResult<T, B>>::Output;
1140
1141            /// # Panics
1142            ///
1143            /// Panics when the operator is applied — not at `.materialize()` — if
1144            /// `rhs` is zero.
1145            #[inline]
1146            fn div(self, rhs: T) -> Self::Output {
1147                (&self).div(rhs)
1148            }
1149        }
1150    };
1151}
1152
1153macro_rules! impl_exp {
1154    ($ty:ident) => {
1155        impl<T, B> $ty<T, B>
1156        where
1157            T: FloatLike,
1158            B: Backend,
1159        {
1160            /// Computes `e^x` for each element.
1161            ///
1162            /// # Examples
1163            ///
1164            /// ```
1165            /// use candela::Tensor;
1166            ///
1167            /// let t = Tensor::from_slice(&[0.0_f64], &[1]);
1168            /// assert_eq!(t.exp().materialize().data(), &[1.0]); // e^0 == 1
1169            /// ```
1170            #[inline]
1171            pub fn exp(&self) -> <$ty<T, B> as UnaryResult<T, B>>::Output {
1172                <$ty<T, B> as UnaryResult<T, B>>::wrap(exp_impl(self))
1173            }
1174        }
1175    };
1176}
1177
1178macro_rules! impl_ln {
1179    ($ty:ident) => {
1180        impl<T, B> $ty<T, B>
1181        where
1182            T: FloatLike,
1183            B: Backend,
1184        {
1185            /// Computes the natural logarithm of each element.
1186            ///
1187            /// Elements `<= 0` follow the platform `ln` behavior: `-inf` at zero, `NaN` below.
1188            ///
1189            /// # Examples
1190            ///
1191            /// ```
1192            /// use candela::Tensor;
1193            ///
1194            /// let t = Tensor::from_slice(&[1.0_f64], &[1]);
1195            /// assert_eq!(t.ln().materialize().data(), &[0.0]); // ln(1) == 0
1196            /// ```
1197            #[inline]
1198            pub fn ln(&self) -> <$ty<T, B> as UnaryResult<T, B>>::Output {
1199                <$ty<T, B> as UnaryResult<T, B>>::wrap(ln_impl(self))
1200            }
1201        }
1202    };
1203}
1204
1205macro_rules! impl_log2 {
1206    ($ty:ident) => {
1207        impl<T, B> $ty<T, B>
1208        where
1209            T: FloatLike,
1210            B: Backend,
1211        {
1212            /// Computes the base-2 logarithm of each element.
1213            ///
1214            /// Elements `<= 0` follow the platform `log2` behavior: `-inf` at zero, `NaN` below.
1215            ///
1216            /// # Examples
1217            ///
1218            /// ```
1219            /// use candela::Tensor;
1220            ///
1221            /// let t = Tensor::from_slice(&[8.0_f64], &[1]);
1222            /// assert_eq!(t.log2().materialize().data(), &[3.0]); // log2(8) == 3
1223            /// ```
1224            #[inline]
1225            pub fn log2(&self) -> <$ty<T, B> as UnaryResult<T, B>>::Output {
1226                <$ty<T, B> as UnaryResult<T, B>>::wrap(log2_impl(self))
1227            }
1228        }
1229    };
1230}
1231
1232macro_rules! impl_relu {
1233    ($ty:ident) => {
1234        impl<T, B> $ty<T, B>
1235        where
1236            T: FloatLike,
1237            B: Backend,
1238        {
1239            /// Applies the rectified linear unit (`relu`): `max(x, 0.0)` for each element.
1240            ///
1241            /// # Examples
1242            ///
1243            /// ```
1244            /// use candela::Tensor;
1245            ///
1246            /// // Negative values clamp to zero; non-negative values pass through.
1247            /// let t = Tensor::from_slice(&[-2.0_f64, -0.5, 0.0, 1.5], &[4]);
1248            /// assert_eq!(t.relu().materialize().data(), &[0.0, 0.0, 0.0, 1.5]);
1249            /// ```
1250            #[inline]
1251            pub fn relu(&self) -> <$ty<T, B> as UnaryResult<T, B>>::Output {
1252                <$ty<T, B> as UnaryResult<T, B>>::wrap(relu_impl(self))
1253            }
1254        }
1255    };
1256}
1257
1258macro_rules! impl_tanh {
1259    ($ty:ident) => {
1260        impl<T, B> $ty<T, B>
1261        where
1262            T: FloatLike,
1263            B: Backend,
1264        {
1265            /// Applies the hyperbolic tangent to each element, mapping values into `(-1, 1)`.
1266            ///
1267            /// # Examples
1268            ///
1269            /// ```
1270            /// use candela::Tensor;
1271            ///
1272            /// let t = Tensor::from_scalar(0.0_f64, &[1]);
1273            /// assert_eq!(*t.tanh().materialize().item(), 0.0); // tanh(0) == 0
1274            /// ```
1275            #[inline]
1276            pub fn tanh(&self) -> <$ty<T, B> as UnaryResult<T, B>>::Output {
1277                <$ty<T, B> as UnaryResult<T, B>>::wrap(tanh_impl(self))
1278            }
1279        }
1280    };
1281}
1282
1283macro_rules! impl_unary_scalar_ops {
1284    ($ty:ident) => {
1285        impl_exp!($ty);
1286        impl_ln!($ty);
1287        impl_log2!($ty);
1288        impl_relu!($ty);
1289        impl_tanh!($ty);
1290    };
1291}
1292
1293macro_rules! impl_op_scalar {
1294    ($ty:ident) => {
1295        impl_add_scalar!($ty);
1296        impl_sub_scalar!($ty);
1297        impl_div_scalar!($ty);
1298        impl_mul_scalar!($ty);
1299    };
1300}
1301
1302//////////////////////////////////////////////////////////////
1303
1304macro_rules! impl_add_assign_scalar {
1305    ($ty:ident) => {
1306        impl<T, B> AddAssign<T> for $ty<T, B>
1307        where
1308            T: NumericOp,
1309            B: Backend,
1310        {
1311            #[inline]
1312            fn add_assign(&mut self, rhs: T) {
1313                *self = add_scalar_impl(&*self, rhs);
1314            }
1315        }
1316    };
1317}
1318
1319macro_rules! impl_sub_assign_scalar {
1320    ($ty:ident) => {
1321        impl<T, B> SubAssign<T> for $ty<T, B>
1322        where
1323            T: NumericOp + Neg<Output = T>,
1324            B: Backend,
1325        {
1326            #[inline]
1327            fn sub_assign(&mut self, rhs: T) {
1328                *self = sub_scalar_impl(&*self, rhs);
1329            }
1330        }
1331    };
1332}
1333
1334macro_rules! impl_mul_assign_scalar {
1335    ($ty:ident) => {
1336        impl<T, B> MulAssign<T> for $ty<T, B>
1337        where
1338            T: NumericOp,
1339            B: Backend,
1340        {
1341            #[inline]
1342            fn mul_assign(&mut self, rhs: T) {
1343                *self = mul_scalar_impl(&*self, rhs);
1344            }
1345        }
1346    };
1347}
1348
1349macro_rules! impl_div_assign_scalar {
1350    ($ty:ident) => {
1351        impl<T, B> DivAssign<T> for $ty<T, B>
1352        where
1353            T: NumericOp,
1354            B: Backend,
1355        {
1356            #[inline]
1357            fn div_assign(&mut self, rhs: T) {
1358                *self = div_scalar_impl(&*self, rhs);
1359            }
1360        }
1361    };
1362}
1363
1364macro_rules! impl_op_assign_scalar {
1365    ($ty:ident) => {
1366        impl_add_assign_scalar!($ty);
1367        impl_sub_assign_scalar!($ty);
1368        impl_mul_assign_scalar!($ty);
1369        impl_div_assign_scalar!($ty);
1370    };
1371}
1372
1373//////////////////////////////////////////////////////////////
1374
1375macro_rules! impl_tensor_binop {
1376    ($trait:ident, $method:ident, $impl_fn:ident, $lhs:ident, $rhs:ident) => {
1377        impl<T, B> $trait<&$rhs<T, B>> for &$lhs<T, B>
1378        where
1379            T: NumericOp,
1380            B: Backend,
1381            $lhs<T, B>: BinaryResult<$rhs<T, B>, T, B>,
1382        {
1383            type Output = <$lhs<T, B> as BinaryResult<$rhs<T, B>, T, B>>::Output;
1384
1385            /// Applies the operation element-wise, broadcasting if the shapes are compatible.
1386            ///
1387            /// A `SkeletonSlot` anywhere in either operand makes the result a
1388            /// [`SkeletonPromise`] instead of a [`TensorPromise`].
1389            ///
1390            /// # Panics
1391            ///
1392            /// Panics when the operator is applied — not at `.materialize()` — if the
1393            /// shapes are not broadcast-compatible.
1394            #[inline]
1395            fn $method(self, rhs: &$rhs<T, B>) -> Self::Output {
1396                <$lhs<T, B> as BinaryResult<$rhs<T, B>, T, B>>::wrap($impl_fn(self, rhs))
1397            }
1398        }
1399
1400        impl<T, B> $trait<$rhs<T, B>> for &$lhs<T, B>
1401        where
1402            T: NumericOp,
1403            B: Backend,
1404            $lhs<T, B>: BinaryResult<$rhs<T, B>, T, B>,
1405        {
1406            type Output = <$lhs<T, B> as BinaryResult<$rhs<T, B>, T, B>>::Output;
1407
1408            /// Applies the operation element-wise, broadcasting if the shapes are compatible.
1409            ///
1410            /// # Panics
1411            ///
1412            /// Panics when the operator is applied — not at `.materialize()` — if the
1413            /// shapes are not broadcast-compatible.
1414            #[inline]
1415            fn $method(self, rhs: $rhs<T, B>) -> Self::Output {
1416                <$lhs<T, B> as BinaryResult<$rhs<T, B>, T, B>>::wrap($impl_fn(self, &rhs))
1417            }
1418        }
1419
1420        impl<T, B> $trait<&$rhs<T, B>> for $lhs<T, B>
1421        where
1422            T: NumericOp,
1423            B: Backend,
1424            $lhs<T, B>: BinaryResult<$rhs<T, B>, T, B>,
1425        {
1426            type Output = <$lhs<T, B> as BinaryResult<$rhs<T, B>, T, B>>::Output;
1427
1428            /// Applies the operation element-wise, broadcasting if the shapes are compatible.
1429            ///
1430            /// # Panics
1431            ///
1432            /// Panics when the operator is applied — not at `.materialize()` — if the
1433            /// shapes are not broadcast-compatible.
1434            #[inline]
1435            fn $method(self, rhs: &$rhs<T, B>) -> Self::Output {
1436                <$lhs<T, B> as BinaryResult<$rhs<T, B>, T, B>>::wrap($impl_fn(&self, rhs))
1437            }
1438        }
1439
1440        impl<T, B> $trait<$rhs<T, B>> for $lhs<T, B>
1441        where
1442            T: NumericOp,
1443            B: Backend,
1444            $lhs<T, B>: BinaryResult<$rhs<T, B>, T, B>,
1445        {
1446            type Output = <$lhs<T, B> as BinaryResult<$rhs<T, B>, T, B>>::Output;
1447
1448            /// Applies the operation element-wise, broadcasting if the shapes are compatible.
1449            ///
1450            /// # Panics
1451            ///
1452            /// Panics when the operator is applied — not at `.materialize()` — if the
1453            /// shapes are not broadcast-compatible.
1454            #[inline]
1455            fn $method(self, rhs: $rhs<T, B>) -> Self::Output {
1456                <$lhs<T, B> as BinaryResult<$rhs<T, B>, T, B>>::wrap($impl_fn(&self, &rhs))
1457            }
1458        }
1459    };
1460}
1461
1462macro_rules! impl_tensor_ops {
1463    ($lhs:ident, $rhs:ident) => {
1464        impl_tensor_binop!(Add, add, add_tensor_impl, $lhs, $rhs);
1465        impl_tensor_binop!(Sub, sub, sub_tensor_impl, $lhs, $rhs);
1466        impl_tensor_binop!(Mul, mul, mul_tensor_impl, $lhs, $rhs);
1467        impl_tensor_binop!(Div, div, div_tensor_impl, $lhs, $rhs);
1468    };
1469}
1470
1471// Cross product: every operand against every operand. The taint algebra
1472// (`BinaryResult`) decides each cell's output type, so a `SkeletonSlot` on
1473// either side yields a `SkeletonPromise`.
1474macro_rules! impl_tensor_ops_cross {
1475    ([$($ty:ident),+ $(,)?]) => {
1476        impl_tensor_ops_cross!(@rows [$($ty),+] [$($ty),+]);
1477    };
1478    (@rows [$($lhs:ident),+] $rhs:tt) => {
1479        $( impl_tensor_ops_cross!(@row $lhs $rhs); )+
1480    };
1481    (@row $lhs:ident [$($rhs:ident),+]) => {
1482        $( impl_tensor_ops!($lhs, $rhs); )+
1483    };
1484}
1485
1486//////////////////////////////////////////////////////////////
1487
1488macro_rules! impl_matmul {
1489    ($ty:ident) => {
1490        impl<T, B> $ty<T, B>
1491        where
1492            T: CanMatMul,
1493            B: Backend,
1494        {
1495            /// Matrix-multiplies, following NumPy's `matmul` conventions.
1496            ///
1497            /// For two 2-D tensors, it's as one would expect: `[m, k] @ [k, n]`
1498            /// gives `[m, n]`, and the inner dimension `k` of both must agree.
1499            ///
1500            /// Higher ranks (3-D, 4-D, and so on) are treated as batches of 2-D
1501            /// matrices, broadcasting the leading axes where needed. So `[b, m, k] @ [k, n]`
1502            /// gives `[b, m, n]`, because it's the same as doing `[b, m, k] @ [b, k, n]`.
1503            ///
1504            /// A 1-D operand is promoted to 2-D for the operation, then the added
1505            /// axis is dropped from the result:
1506            /// - `[k] @ [k]` contracts to a one-element tensor (a dot product).
1507            /// - `[k] @ [.., k, n]` gives `[.., n]` (vector times matrix).
1508            /// - `[.., m, k] @ [k]` gives `[.., m]` (matrix times vector).
1509            ///
1510            /// # Examples
1511            ///
1512            /// ```
1513            /// use candela::{Tensor, Dimension};
1514            ///
1515            /// let a = Tensor::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
1516            /// let b = Tensor::from_slice(&[1.0, 0.0, 0.0, 1.0, 1.0, 0.0], &[3, 2]);
1517            /// let c = a.matmul(&b).unwrap().materialize();
1518            ///
1519            /// assert_eq!(c.shape(), &[2, 2]);
1520            /// assert_eq!(c.data(), &[4.0, 2.0, 10.0, 5.0]);
1521            /// ```
1522            ///
1523            /// A 1-D right-hand side contracts the last axis away:
1524            ///
1525            /// ```
1526            /// use candela::{Tensor, Dimension};
1527            ///
1528            /// let m = Tensor::from_slice(&[1.0, 2.0, 3.0, 4.0], &[2, 2]);
1529            /// let v = Tensor::from_slice(&[1.0, 1.0], &[2]);
1530            /// let r = m.matmul(&v).unwrap().materialize();
1531            ///
1532            /// assert_eq!(r.shape(), &[2]);
1533            /// assert_eq!(r.data(), &[3.0, 7.0]);
1534            /// ```
1535            ///
1536            /// # Errors
1537            ///
1538            /// Returns [`OpError::CannotMatMul`] if the inner dimensions don't agree, or
1539            /// [`OpError::CannotBroadcast`] if the batch axes aren't broadcast-compatible.
1540            #[inline]
1541            pub fn matmul<D>(
1542                &self,
1543                rhs: &D,
1544            ) -> Result<<$ty<T, B> as BinaryResult<D, T, B>>::Output, OpError>
1545            where
1546                D: Operand<T, B>,
1547                $ty<T, B>: BinaryResult<D, T, B>,
1548            {
1549                matmul_tensor_impl(self, rhs).map(<$ty<T, B> as BinaryResult<D, T, B>>::wrap)
1550            }
1551        }
1552    };
1553}
1554
1555//////////////////////////////////////////////////////////////
1556
1557macro_rules! impl_sum {
1558    ($ty:ident) => {
1559        impl<T, B> $ty<T, B>
1560        where
1561            T: NumericOp,
1562            B: Backend,
1563        {
1564            /// Sums every element, producing a one-element tensor.
1565            ///
1566            /// To reduce along a single axis instead, see
1567            /// [`.sum_axis()`][Self::sum_axis].
1568            ///
1569            /// # Examples
1570            ///
1571            /// ```
1572            /// use candela::Tensor;
1573            ///
1574            /// let t = Tensor::from_slice(&[1.0, 2.0, 3.0, 4.0], &[2, 2]);
1575            /// assert_eq!(t.sum().materialize().data(), &[10.0]);
1576            /// ```
1577            #[inline]
1578            pub fn sum(&self) -> <$ty<T, B> as UnaryResult<T, B>>::Output {
1579                <$ty<T, B> as UnaryResult<T, B>>::wrap(sum_impl(self))
1580            }
1581        }
1582    };
1583}
1584
1585macro_rules! impl_sum_axis {
1586    ($ty:ident) => {
1587        impl<T, B> $ty<T, B>
1588        where
1589            T: NumericOp,
1590            B: Backend,
1591        {
1592            /// Sums along a single axis.
1593            ///
1594            /// `axis` selects the axis to collapse and may be negative to count from
1595            /// the end. With `keep_dims = false` that axis is removed from the shape;
1596            /// with `keep_dims = true` it is kept as a size-1 axis, which leaves the
1597            /// result broadcastable against the input. To reduce the whole tensor,
1598            /// see [`.sum()`][Self::sum].
1599            ///
1600            /// # Examples
1601            ///
1602            /// ```
1603            /// use candela::{Tensor, Dimension};
1604            ///
1605            /// let t = Tensor::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
1606            ///
1607            /// let dropped = t.sum_axis(0, false).unwrap().materialize();
1608            /// assert_eq!(dropped.shape(), &[3]);
1609            /// assert_eq!(dropped.data(), &[5.0, 7.0, 9.0]);
1610            ///
1611            /// // keep_dims = true leaves a size-1 axis in place.
1612            /// let kept = t.sum_axis(0, true).unwrap().materialize();
1613            /// assert_eq!(kept.shape(), &[1, 3]);
1614            /// ```
1615            ///
1616            /// # Errors
1617            ///
1618            /// Returns [`OpError::AxesOutOfBounds`] if `axis` is outside the tensor's rank.
1619            #[inline]
1620            pub fn sum_axis(
1621                &self,
1622                axis: isize,
1623                keep_dims: bool,
1624            ) -> Result<<$ty<T, B> as UnaryResult<T, B>>::Output, OpError> {
1625                sum_axis_impl(self, axis, keep_dims).map(<$ty<T, B> as UnaryResult<T, B>>::wrap)
1626            }
1627        }
1628    };
1629}
1630
1631macro_rules! impl_max {
1632    ($ty:ident) => {
1633        impl<T, B> $ty<T, B>
1634        where
1635            T: NumericOp,
1636            B: Backend,
1637        {
1638            /// Returns the largest element, as a one-element tensor.
1639            ///
1640            /// To take the maximum along a single axis instead, see
1641            /// [`.max_axis()`][Self::max_axis].
1642            ///
1643            /// # Examples
1644            ///
1645            /// ```
1646            /// use candela::Tensor;
1647            ///
1648            /// let t = Tensor::from_slice(&[3.0, 1.0, 4.0, 1.0, 5.0, 2.0], &[2, 3]);
1649            /// assert_eq!(t.max().materialize().data(), &[5.0]);
1650            /// ```
1651            #[inline]
1652            pub fn max(&self) -> <$ty<T, B> as UnaryResult<T, B>>::Output {
1653                <$ty<T, B> as UnaryResult<T, B>>::wrap(max_impl(self))
1654            }
1655        }
1656    };
1657}
1658
1659macro_rules! impl_max_axis {
1660    ($ty:ident) => {
1661        impl<T, B> $ty<T, B>
1662        where
1663            T: NumericOp,
1664            B: Backend,
1665        {
1666            /// Takes the maximum along a single axis.
1667            ///
1668            /// `axis` and `keep_dims` behave exactly as in
1669            /// [`.sum_axis()`][Self::sum_axis]. To reduce the whole tensor, see
1670            /// [`.max()`][Self::max].
1671            ///
1672            /// # Examples
1673            ///
1674            /// ```
1675            /// use candela::{Tensor, Dimension};
1676            ///
1677            /// let t = Tensor::from_slice(&[3.0, 1.0, 4.0, 1.0, 5.0, 2.0], &[2, 3]);
1678            /// let m = t.max_axis(1, false).unwrap().materialize();
1679            ///
1680            /// assert_eq!(m.shape(), &[2]);
1681            /// assert_eq!(m.data(), &[4.0, 5.0]);
1682            /// ```
1683            ///
1684            /// # Errors
1685            ///
1686            /// Returns [`OpError::AxesOutOfBounds`] if `axis` is outside the tensor's rank.
1687            #[inline]
1688            pub fn max_axis(
1689                &self,
1690                axis: isize,
1691                keep_dims: bool,
1692            ) -> Result<<$ty<T, B> as UnaryResult<T, B>>::Output, OpError> {
1693                max_axis_impl(self, axis, keep_dims).map(<$ty<T, B> as UnaryResult<T, B>>::wrap)
1694            }
1695        }
1696    };
1697}
1698
1699macro_rules! impl_mean {
1700    ($ty:ident) => {
1701        impl<T, B> $ty<T, B>
1702        where
1703            T: FloatLike,
1704            B: Backend,
1705        {
1706            /// Averages every element, producing a one-element tensor.
1707            ///
1708            /// To average along a single axis instead, see
1709            /// [`.mean_axis()`][Self::mean_axis].
1710            ///
1711            /// # Examples
1712            ///
1713            /// ```
1714            /// use candela::Tensor;
1715            ///
1716            /// let t = Tensor::from_slice(&[1.0, 2.0, 3.0, 4.0], &[2, 2]);
1717            /// assert_eq!(t.mean().materialize().data(), &[2.5]);
1718            /// ```
1719            #[inline]
1720            pub fn mean(&self) -> <$ty<T, B> as UnaryResult<T, B>>::Output {
1721                <$ty<T, B> as UnaryResult<T, B>>::wrap(mean_impl(self))
1722            }
1723        }
1724    };
1725}
1726
1727macro_rules! impl_mean_axis {
1728    ($ty:ident) => {
1729        impl<T, B> $ty<T, B>
1730        where
1731            T: FloatLike,
1732            B: Backend,
1733        {
1734            /// Averages along a single axis.
1735            ///
1736            /// `axis` and `keep_dims` behave exactly as in
1737            /// [`.sum_axis()`][Self::sum_axis]. To average the whole tensor, see
1738            /// [`.mean()`][Self::mean].
1739            ///
1740            /// # Examples
1741            ///
1742            /// ```
1743            /// use candela::{Tensor, Dimension};
1744            ///
1745            /// let t = Tensor::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]);
1746            /// let m = t.mean_axis(1, false).unwrap().materialize();
1747            ///
1748            /// assert_eq!(m.shape(), &[2]);
1749            /// assert_eq!(m.data(), &[2.0, 5.0]);
1750            /// ```
1751            ///
1752            /// # Errors
1753            ///
1754            /// Returns [`OpError::AxesOutOfBounds`] if `axis` is outside the tensor's rank.
1755            #[inline]
1756            pub fn mean_axis(
1757                &self,
1758                axis: isize,
1759                keep_dims: bool,
1760            ) -> Result<<$ty<T, B> as UnaryResult<T, B>>::Output, OpError> {
1761                mean_axis_impl(self, axis, keep_dims).map(<$ty<T, B> as UnaryResult<T, B>>::wrap)
1762            }
1763        }
1764    };
1765}
1766
1767//////////////////////////////////////////////////////////////
1768
1769macro_rules! impl_tensor_assign_binop {
1770    ($trait:ident, $method:ident, $impl_fn:ident, $rhs:ident) => {
1771        impl<T, B> $trait<$rhs<T, B>> for TensorPromise<T, B>
1772        where
1773            T: NumericOp,
1774            B: Backend,
1775        {
1776            #[inline]
1777            fn $method(&mut self, rhs: $rhs<T, B>) {
1778                *self = $impl_fn(&*self, &rhs);
1779            }
1780        }
1781
1782        impl<T, B> $trait<&$rhs<T, B>> for TensorPromise<T, B>
1783        where
1784            T: NumericOp,
1785            B: Backend,
1786        {
1787            #[inline]
1788            fn $method(&mut self, rhs: &$rhs<T, B>) {
1789                *self = $impl_fn(&*self, rhs);
1790            }
1791        }
1792    };
1793}
1794
1795macro_rules! impl_tensor_assign_ops {
1796    ($rhs:ident) => {
1797        impl_tensor_assign_binop!(AddAssign, add_assign, add_tensor_impl, $rhs);
1798        impl_tensor_assign_binop!(SubAssign, sub_assign, sub_tensor_impl, $rhs);
1799        impl_tensor_assign_binop!(MulAssign, mul_assign, mul_tensor_impl, $rhs);
1800        impl_tensor_assign_binop!(DivAssign, div_assign, div_tensor_impl, $rhs);
1801    };
1802}
1803
1804//////////////////////////////////////////////////////////////
1805
1806macro_rules! impl_all_ops {
1807    ($ty:ident) => {
1808        impl_reshape_like!($ty);
1809        impl_unary_scalar_ops!($ty);
1810        impl_op_scalar!($ty);
1811        impl_matmul!($ty);
1812        impl_sum!($ty);
1813        impl_sum_axis!($ty);
1814        impl_max!($ty);
1815        impl_max_axis!($ty);
1816        impl_mean!($ty);
1817        impl_mean_axis!($ty);
1818    };
1819}
1820
1821impl_all_ops!(Tensor);
1822impl_all_ops!(TensorPromise);
1823impl_all_ops!(CachedTensorPromise);
1824impl_all_ops!(BakedPromise);
1825impl_all_ops!(SkeletonSlot);
1826impl_all_ops!(SkeletonPromise);
1827
1828impl_tensor_ops_cross!([
1829    Tensor,
1830    TensorPromise,
1831    CachedTensorPromise,
1832    BakedPromise,
1833    SkeletonSlot,
1834    SkeletonPromise,
1835]);
1836
1837impl_op_assign_scalar!(TensorPromise);
1838
1839impl_tensor_assign_ops!(Tensor);
1840impl_tensor_assign_ops!(TensorPromise);
1841impl_tensor_assign_ops!(CachedTensorPromise);
1842impl_tensor_assign_ops!(BakedPromise);