## Coverage Report

#### Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /home/noah/src/trueno/src/backends/gpu/tensor_view.rs

    Line
    Count
    Source
    1
    //! TensorView - GPU Memory Layout Abstraction
    2
    //!
    3
    //! Provides a view into GPU buffer memory with shape, stride, and layout information.
    4
    //! Enables zero-copy slicing and transposition operations.
    5
    //!
    6
    //! # cuda-tile-behavior.md References
    7
    //!
    8
    //! - Section 3.2: Two-Level Memory Hierarchy
    9
    //! - Falsification tests #31-40: TensorView correctness
    10
    //!
    11
    //! # Academic Foundation
    12
    //!
    13
    //! Based on Halide (PLDI 2013): Schedule/algorithm separation improves portability.
    14
    15
    use std::marker::PhantomData;
    16
    use std::ops::Range;
    17
    18
    /// Memory layout for tensor storage
    19
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
    20
    pub enum MemoryLayout {
    21
        /// Row-major (C-style): last dimension varies fastest
    22
        #[default]
    23
        RowMajor,
    24
        /// Column-major (Fortran-style): first dimension varies fastest
    25
        ColumnMajor,
    26
        /// Tiled layout for GPU shared memory optimization
    27
        Tiled {
    28
            /// Tile dimensions
    29
            tile_size: [usize; 2],
    30
        },
    31
    }
    32
    33
    /// A view into a contiguous memory region with shape and stride information.
    34
    ///
    35
    /// TensorView does not own the data - it provides a structured view over
    36
    /// existing memory, enabling zero-copy operations like slicing and transposition.
    37
    ///
    38
    /// # Type Parameters
    39
    ///
    40
    /// * `T` - Element type (typically f32 for GPU compute)
    41
    ///
    42
    /// # cuda-tile-behavior.md References
    43
    ///
    44
    /// - Falsification test #31: TensorView preserves data integrity
    45
    /// - Falsification test #32: Slicing produces correct views
    46
    /// - Falsification test #33: Transpose swaps dimensions correctly
    47
    #[derive(Debug)]
    48
    pub struct TensorView<T> {
    49
        /// Shape of the tensor (up to 4 dimensions: N, C, H, W)
    50
        shape: [usize; 4],
    51
        /// Strides for each dimension (in elements, not bytes)
    52
        strides: [usize; 4],
    53
        /// Offset from the start of the buffer (in elements)
    54
        offset: usize,
    55
        /// Memory layout hint for optimization
    56
        layout: MemoryLayout,
    57
        /// Number of active dimensions (1-4)
    58
        ndim: usize,
    59
        /// Phantom data for type safety
    60
        _marker: PhantomData<T>,
    61
    }
    62
    63
    impl<T> TensorView<T> {
    64
        /// Create a new TensorView with the given shape.
    65
        ///
    66
        /// Strides are computed automatically based on row-major layout.
    67
        ///
    68
        /// # Arguments
    69
        ///
    70
        /// * `shape` - Shape of the tensor (unused dimensions should be 1)
    71
        ///
    72
        /// # Examples
    73
        ///
    74
        /// ```ignore
    75
        /// let view = TensorView::<f32>::new([2, 3, 4, 1]); // 2x3x4 tensor
    76
        /// assert_eq!(view.numel(), 24);
    77
        /// ```
    78
    0
        pub fn new(shape: [usize; 4]) -> Self {
    79
    0
            let ndim = Self::compute_ndim(&shape);
    80
    0
            let strides = Self::compute_row_major_strides(&shape);
    81
    0
            Self {
    82
    0
                shape,
    83
    0
                strides,
    84
    0
                offset: 0,
    85
    0
                layout: MemoryLayout::RowMajor,
    86
    0
                ndim,
    87
    0
                _marker: PhantomData,
    88
    0
            }
    89
    0
        }
    90
    91
        /// Create a TensorView with explicit strides.
    92
        ///
    93
        /// # Arguments
    94
        ///
    95
        /// * `shape` - Shape of the tensor
    96
        /// * `strides` - Strides for each dimension (in elements)
    97
    0
        pub fn with_strides(shape: [usize; 4], strides: [usize; 4]) -> Self {
    98
    0
            let ndim = Self::compute_ndim(&shape);
    99
    0
            Self {
    100
    0
                shape,
    101
    0
                strides,
    102
    0
                offset: 0,
    103
    0
                layout: MemoryLayout::RowMajor,
    104
    0
                ndim,
    105
    0
                _marker: PhantomData,
    106
    0
            }
    107
    0
        }
    108
    109
        /// Create a 1D TensorView.
    110
    0
        pub fn new_1d(len: usize) -> Self {
    111
    0
            Self::new([len, 1, 1, 1])
    112
    0
        }
    113
    114
        /// Create a 2D TensorView (matrix).
    115
    0
        pub fn new_2d(rows: usize, cols: usize) -> Self {
    116
    0
            Self::new([rows, cols, 1, 1])
    117
    0
        }
    118
    119
        /// Create a 3D TensorView.
    120
    0
        pub fn new_3d(d0: usize, d1: usize, d2: usize) -> Self {
    121
    0
            Self::new([d0, d1, d2, 1])
    122
    0
        }
    123
    124
        /// Create a 4D TensorView.
    125
    0
        pub fn new_4d(d0: usize, d1: usize, d2: usize, d3: usize) -> Self {
    126
    0
            Self::new([d0, d1, d2, d3])
    127
    0
        }
    128
    129
        /// Get the shape of the tensor.
    130
    0
        pub fn shape(&self) -> &[usize; 4] {
    131
    0
            &self.shape
    132
    0
        }
    133
    134
        /// Get the strides of the tensor.
    135
    0
        pub fn strides(&self) -> &[usize; 4] {
    136
    0
            &self.strides
    137
    0
        }
    138
    139
        /// Get the offset from the start of the buffer.
    140
    0
        pub fn offset(&self) -> usize {
    141
    0
            self.offset
    142
    0
        }
    143
    144
        /// Get the memory layout.
    145
    0
        pub fn layout(&self) -> MemoryLayout {
    146
    0
            self.layout
    147
    0
        }
    148
    149
        /// Get the number of active dimensions.
    150
    0
        pub fn ndim(&self) -> usize {
    151
    0
            self.ndim
    152
    0
        }
    153
    154
        /// Get the total number of elements.
    155
    0
        pub fn numel(&self) -> usize {
    156
    0
            self.shape.iter().product()
    157
    0
        }
    158
    159
        /// Check if the tensor is contiguous in memory.
    160
        ///
    161
        /// A tensor is contiguous if elements are stored without gaps
    162
        /// in row-major order.
    163
    0
        pub fn is_contiguous(&self) -> bool {
    164
    0
            let expected_strides = Self::compute_row_major_strides(&self.shape);
    165
    0
            self.strides == expected_strides
    166
    0
        }
    167
    168
        /// Check if the tensor is empty (has zero elements).
    169
    0
        pub fn is_empty(&self) -> bool {
    170
    0
            self.numel() == 0
    171
    0
        }
    172
    173
        /// Get dimension size at the given index.
    174
        ///
    175
        /// # Panics
    176
        ///
    177
        /// Panics if `dim >= 4`.
    178
    0
        pub fn dim(&self, dim: usize) -> usize {
    179
    0
            self.shape[dim]
    180
    0
        }
    181
    182
        /// Get stride at the given dimension.
    183
        ///
    184
        /// # Panics
    185
        ///
    186
        /// Panics if `dim >= 4`.
    187
    0
        pub fn stride(&self, dim: usize) -> usize {
    188
    0
            self.strides[dim]
    189
    0
        }
    190
    191
        /// Create a slice of this tensor along the first dimension.
    192
        ///
    193
        /// # Arguments
    194
        ///
    195
        /// * `range` - Range of indices to include
    196
        ///
    197
        /// # Returns
    198
        ///
    199
        /// A new TensorView representing the slice.
    200
        ///
    201
        /// # cuda-tile-behavior.md References
    202
        ///
    203
        /// - Falsification test #32: Slicing produces correct views
    204
    0
        pub fn slice(&self, range: Range<usize>) -> Self {
    205
    0
            assert!(range.end <= self.shape[0], "Slice range out of bounds");
    206
    0
            let new_offset = self.offset + range.start * self.strides[0];
    207
    0
            let mut new_shape = self.shape;
    208
    0
            new_shape[0] = range.end - range.start;
    209
    210
    0
            Self {
    211
    0
                shape: new_shape,
    212
    0
                strides: self.strides,
    213
    0
                offset: new_offset,
    214
    0
                layout: self.layout,
    215
    0
                ndim: self.ndim,
    216
    0
                _marker: PhantomData,
    217
    0
            }
    218
    0
        }
    219
    220
        /// Create a slice along a specific dimension.
    221
        ///
    222
        /// # Arguments
    223
        ///
    224
        /// * `dim` - Dimension to slice along
    225
        /// * `range` - Range of indices to include
    226
    0
        pub fn slice_dim(&self, dim: usize, range: Range<usize>) -> Self {
    227
    0
            assert!(dim < 4, "Dimension out of bounds");
    228
    0
            assert!(range.end <= self.shape[dim], "Slice range out of bounds");
    229
    230
    0
            let new_offset = self.offset + range.start * self.strides[dim];
    231
    0
            let mut new_shape = self.shape;
    232
    0
            new_shape[dim] = range.end - range.start;
    233
    234
    0
            Self {
    235
    0
                shape: new_shape,
    236
    0
                strides: self.strides,
    237
    0
                offset: new_offset,
    238
    0
                layout: self.layout,
    239
    0
                ndim: self.ndim,
    240
    0
                _marker: PhantomData,
    241
    0
            }
    242
    0
        }
    243
    244
        /// Transpose the tensor by swapping two dimensions.
    245
        ///
    246
        /// # Arguments
    247
        ///
    248
        /// * `dim0` - First dimension to swap
    249
        /// * `dim1` - Second dimension to swap
    250
        ///
    251
        /// # Returns
    252
        ///
    253
        /// A new TensorView with swapped dimensions.
    254
        ///
    255
        /// # cuda-tile-behavior.md References
    256
        ///
    257
        /// - Falsification test #33: Transpose swaps dimensions correctly
    258
    0
        pub fn transpose(&self, dim0: usize, dim1: usize) -> Self {
    259
    0
            assert!(dim0 < 4 && dim1 < 4, "Dimension out of bounds");
    260
    261
    0
            let mut new_shape = self.shape;
    262
    0
            let mut new_strides = self.strides;
    263
    0
            new_shape.swap(dim0, dim1);
    264
    0
            new_strides.swap(dim0, dim1);
    265
    266
    0
            Self {
    267
    0
                shape: new_shape,
    268
    0
                strides: new_strides,
    269
    0
                offset: self.offset,
    270
    0
                layout: self.layout,
    271
    0
                ndim: self.ndim,
    272
    0
                _marker: PhantomData,
    273
    0
            }
    274
    0
        }
    275
    276
        /// Reshape the tensor to a new shape.
    277
        ///
    278
        /// # Arguments
    279
        ///
    280
        /// * `new_shape` - New shape (must have same number of elements)
    281
        ///
    282
        /// # Returns
    283
        ///
    284
        /// A new TensorView with the new shape, or None if reshape is invalid.
    285
    0
        pub fn reshape(&self, new_shape: [usize; 4]) -> Option<Self> {
    286
    0
            let new_numel: usize = new_shape.iter().product();
    287
    0
            if new_numel != self.numel() {
    288
    0
                return None;
    289
    0
            }
    290
    291
            // Reshape requires contiguous memory
    292
    0
            if !self.is_contiguous() {
    293
    0
                return None;
    294
    0
            }
    295
    296
    0
            Some(Self::new(new_shape))
    297
    0
        }
    298
    299
        /// Squeeze dimensions of size 1.
    300
        ///
    301
        /// Returns a view with all size-1 dimensions removed.
    302
    0
        pub fn squeeze(&self) -> Self {
    303
    0
            let mut new_shape = [1usize; 4];
    304
    0
            let mut new_strides = [1usize; 4];
    305
    0
            let mut new_ndim = 0;
    306
    307
    0
            for i in 0..4 {
    308
    0
                if self.shape[i] > 1 {
    309
    0
                    new_shape[new_ndim] = self.shape[i];
    310
    0
                    new_strides[new_ndim] = self.strides[i];
    311
    0
                    new_ndim += 1;
    312
    0
                }
    313
            }
    314
    315
            // If all dimensions were 1, keep at least one
    316
    0
            if new_ndim == 0 {
    317
    0
                new_ndim = 1;
    318
    0
            }
    319
    320
    0
            Self {
    321
    0
                shape: new_shape,
    322
    0
                strides: new_strides,
    323
    0
                offset: self.offset,
    324
    0
                layout: self.layout,
    325
    0
                ndim: new_ndim,
    326
    0
                _marker: PhantomData,
    327
    0
            }
    328
    0
        }
    329
    330
        /// Unsqueeze: add a dimension of size 1 at the specified position.
    331
        ///
    332
        /// # Arguments
    333
        ///
    334
        /// * `dim` - Position to insert the new dimension
    335
    0
        pub fn unsqueeze(&self, dim: usize) -> Option<Self> {
    336
    0
            if dim > self.ndim || self.ndim >= 4 {
    337
    0
                return None;
    338
    0
            }
    339
    340
    0
            let mut new_shape = [1usize; 4];
    341
    0
            let mut new_strides = [1usize; 4];
    342
    343
            // Copy dimensions before the insertion point
    344
            // Using manual loop since we're copying from two separate arrays to two separate arrays
    345
            #[allow(clippy::manual_memcpy)]
    346
    0
            for i in 0..dim {
    347
    0
                new_shape[i] = self.shape[i];
    348
    0
                new_strides[i] = self.strides[i];
    349
    0
            }
    350
    351
            // Insert the new dimension
    352
    0
            new_shape[dim] = 1;
    353
    0
            new_strides[dim] = if dim < self.ndim {
    354
    0
                self.strides[dim] * self.shape[dim]
    355
            } else {
    356
    0
                1
    357
            };
    358
    359
            // Copy remaining dimensions (offset by 1 for insertion)
    360
            #[allow(clippy::manual_memcpy)]
    361
    0
            for i in dim..self.ndim {
    362
    0
                new_shape[i + 1] = self.shape[i];
    363
    0
                new_strides[i + 1] = self.strides[i];
    364
    0
            }
    365
    366
    0
            Some(Self {
    367
    0
                shape: new_shape,
    368
    0
                strides: new_strides,
    369
    0
                offset: self.offset,
    370
    0
                layout: self.layout,
    371
    0
                ndim: self.ndim + 1,
    372
    0
                _marker: PhantomData,
    373
    0
            })
    374
    0
        }
    375
    376
        /// Set the memory layout hint.
    377
    0
        pub fn with_layout(mut self, layout: MemoryLayout) -> Self {
    378
    0
            self.layout = layout;
    379
    0
            self
    380
    0
        }
    381
    382
        /// Compute linear index from multi-dimensional indices.
    383
        ///
    384
        /// # Arguments
    385
        ///
    386
        /// * `indices` - Array of indices for each dimension
    387
        ///
    388
        /// # Returns
    389
        ///
    390
        /// Linear offset into the underlying buffer.
    391
    0
        pub fn linear_index(&self, indices: [usize; 4]) -> usize {
    392
    0
            self.offset
    393
    0
                + indices[0] * self.strides[0]
    394
    0
                + indices[1] * self.strides[1]
    395
    0
                + indices[2] * self.strides[2]
    396
    0
                + indices[3] * self.strides[3]
    397
    0
        }
    398
    399
        /// Compute row-major strides for a given shape.
    400
    0
        fn compute_row_major_strides(shape: &[usize; 4]) -> [usize; 4] {
    401
    0
            let mut strides = [1usize; 4];
    402
            // Strides: s[i] = product of shape[i+1..4]
    403
    0
            strides[3] = 1;
    404
    0
            strides[2] = shape[3];
    405
    0
            strides[1] = shape[3] * shape[2];
    406
    0
            strides[0] = shape[3] * shape[2] * shape[1];
    407
    0
            strides
    408
    0
        }
    409
    410
        /// Compute the number of active dimensions.
    411
    0
        fn compute_ndim(shape: &[usize; 4]) -> usize {
    412
            // Count from the end: find last dimension > 1
    413
    0
            for i in (0..4).rev() {
    414
    0
                if shape[i] > 1 {
    415
    0
                    return i + 1;
    416
    0
                }
    417
            }
    418
    0
            1 // At least 1 dimension
    419
    0
        }
    420
    }
    421
    422
    impl<T> Clone for TensorView<T> {
    423
    0
        fn clone(&self) -> Self {
    424
    0
            Self {
    425
    0
                shape: self.shape,
    426
    0
                strides: self.strides,
    427
    0
                offset: self.offset,
    428
    0
                layout: self.layout,
    429
    0
                ndim: self.ndim,
    430
    0
                _marker: PhantomData,
    431
    0
            }
    432
    0
        }
    433
    }
    434
    435
    impl<T> Default for TensorView<T> {
    436
    0
        fn default() -> Self {
    437
    0
            Self::new([1, 1, 1, 1])
    438
    0
        }
    439
    }
    440
    441
    #[cfg(test)]
    442
    mod tests {
    443
        use super::*;
    444
    445
        // cuda-tile-behavior.md: Falsification test #31
    446
        #[test]
    447
        fn test_tensor_view_creation() {
    448
            let view = TensorView::<f32>::new([2, 3, 4, 5]);
    449
            assert_eq!(view.shape(), &[2, 3, 4, 5]);
    450
            assert_eq!(view.numel(), 120);
    451
            assert_eq!(view.ndim(), 4);
    452
            assert!(view.is_contiguous());
    453
        }
    454
    455
        #[test]
    456
        fn test_tensor_view_1d() {
    457
            let view = TensorView::<f32>::new_1d(100);
    458
            assert_eq!(view.shape(), &[100, 1, 1, 1]);
    459
            assert_eq!(view.numel(), 100);
    460
            assert_eq!(view.ndim(), 1);
    461
        }
    462
    463
        #[test]
    464
        fn test_tensor_view_2d() {
    465
            let view = TensorView::<f32>::new_2d(10, 20);
    466
            assert_eq!(view.shape(), &[10, 20, 1, 1]);
    467
            assert_eq!(view.numel(), 200);
    468
            assert_eq!(view.ndim(), 2);
    469
        }
    470
    471
        #[test]
    472
        fn test_tensor_view_strides() {
    473
            let view = TensorView::<f32>::new([2, 3, 4, 5]);
    474
            // Row-major: strides[i] = product of shape[i+1..]
    475
            assert_eq!(view.strides(), &[60, 20, 5, 1]);
    476
        }
    477
    478
        // cuda-tile-behavior.md: Falsification test #32
    479
        #[test]
    480
        fn test_tensor_view_slice() {
    481
            let view = TensorView::<f32>::new([10, 20, 1, 1]);
    482
            let sliced = view.slice(2..7);
    483
    484
            assert_eq!(sliced.shape(), &[5, 20, 1, 1]);
    485
            assert_eq!(sliced.offset(), 40); // 2 * 20
    486
            assert_eq!(sliced.numel(), 100);
    487
        }
    488
    489
        #[test]
    490
        fn test_tensor_view_slice_dim() {
    491
            let view = TensorView::<f32>::new([10, 20, 30, 1]);
    492
            let sliced = view.slice_dim(1, 5..15);
    493
    494
            assert_eq!(sliced.shape(), &[10, 10, 30, 1]);
    495
            assert_eq!(sliced.offset(), 5 * 30); // offset by 5 in dim 1
    496
        }
    497
    498
        // cuda-tile-behavior.md: Falsification test #33
    499
        #[test]
    500
        fn test_tensor_view_transpose() {
    501
            let view = TensorView::<f32>::new([2, 3, 1, 1]);
    502
            let transposed = view.transpose(0, 1);
    503
    504
            assert_eq!(transposed.shape(), &[3, 2, 1, 1]);
    505
            assert_eq!(transposed.strides(), &[1, 3, 1, 1]); // Swapped strides
    506
            assert!(!transposed.is_contiguous()); // Non-contiguous after transpose
    507
        }
    508
    509
        #[test]
    510
        fn test_tensor_view_reshape() {
    511
            let view = TensorView::<f32>::new([2, 3, 4, 1]);
    512
            let reshaped = view.reshape([6, 4, 1, 1]).unwrap();
    513
    514
            assert_eq!(reshaped.shape(), &[6, 4, 1, 1]);
    515
            assert_eq!(reshaped.numel(), 24);
    516
        }
    517
    518
        #[test]
    519
        fn test_tensor_view_reshape_invalid() {
    520
            let view = TensorView::<f32>::new([2, 3, 4, 1]);
    521
            let result = view.reshape([5, 5, 1, 1]); // 25 != 24
    522
            assert!(result.is_none());
    523
        }
    524
    525
        #[test]
    526
        fn test_tensor_view_squeeze() {
    527
            let view = TensorView::<f32>::new([1, 3, 1, 4]);
    528
            let squeezed = view.squeeze();
    529
    530
            assert_eq!(squeezed.shape()[0], 3);
    531
            assert_eq!(squeezed.shape()[1], 4);
    532
            assert_eq!(squeezed.ndim(), 2);
    533
        }
    534
    535
        #[test]
    536
        fn test_tensor_view_unsqueeze() {
    537
            let view = TensorView::<f32>::new_2d(3, 4);
    538
            let unsqueezed = view.unsqueeze(0).unwrap();
    539
    540
            assert_eq!(unsqueezed.shape(), &[1, 3, 4, 1]);
    541
            assert_eq!(unsqueezed.ndim(), 3);
    542
        }
    543
    544
        #[test]
    545
        fn test_tensor_view_linear_index() {
    546
            let view = TensorView::<f32>::new([2, 3, 4, 1]);
    547
    548
            // First element
    549
            assert_eq!(view.linear_index([0, 0, 0, 0]), 0);
    550
    551
            // Element at [1, 0, 0, 0]
    552
            assert_eq!(view.linear_index([1, 0, 0, 0]), 12); // 1 * 12
    553
    554
            // Element at [0, 1, 0, 0]
    555
            assert_eq!(view.linear_index([0, 1, 0, 0]), 4); // 1 * 4
    556
    557
            // Element at [1, 2, 3, 0]
    558
            assert_eq!(view.linear_index([1, 2, 3, 0]), 12 + 8 + 3); // 23
    559
        }
    560
    561
        #[test]
    562
        fn test_tensor_view_is_empty() {
    563
            let empty = TensorView::<f32>::new([0, 1, 1, 1]);
    564
            assert!(empty.is_empty());
    565
    566
            let non_empty = TensorView::<f32>::new([1, 1, 1, 1]);
    567
            assert!(!non_empty.is_empty());
    568
        }
    569
    570
        #[test]
    571
        fn test_tensor_view_with_strides() {
    572
            let view = TensorView::<f32>::with_strides([2, 3, 1, 1], [6, 2, 1, 1]);
    573
            assert_eq!(view.strides(), &[6, 2, 1, 1]);
    574
            assert!(!view.is_contiguous()); // Custom strides
    575
        }
    576
    577
        #[test]
    578
        fn test_tensor_view_default() {
    579
            let view = TensorView::<f32>::default();
    580
            assert_eq!(view.numel(), 1);
    581
            assert_eq!(view.ndim(), 1);
    582
        }
    583
    584
        #[test]
    585
        fn test_memory_layout() {
    586
            let view = TensorView::<f32>::new([4, 4, 1, 1])
    587
                .with_layout(MemoryLayout::Tiled { tile_size: [2, 2] });
    588
    589
            assert!(matches!(
    590
                view.layout(),
    591
                MemoryLayout::Tiled { tile_size: [2, 2] }
    592
            ));
    593
        }
    594
    595
        #[test]
    596
        fn test_tensor_view_clone() {
    597
            let view = TensorView::<f32>::new([2, 3, 4, 5]);
    598
            let cloned = view.clone();
    599
    600
            assert_eq!(view.shape(), cloned.shape());
    601
            assert_eq!(view.strides(), cloned.strides());
    602
        }
    603
    604
        // cuda-tile-behavior.md: Falsification test #34 - Dimension accessors
    605
        #[test]
    606
        fn test_tensor_view_dim_accessors() {
    607
            let view = TensorView::<f32>::new([2, 3, 4, 5]);
    608
    609
            assert_eq!(view.dim(0), 2);
    610
            assert_eq!(view.dim(1), 3);
    611
            assert_eq!(view.dim(2), 4);
    612
            assert_eq!(view.dim(3), 5);
    613
    614
            assert_eq!(view.stride(0), 60);
    615
            assert_eq!(view.stride(1), 20);
    616
            assert_eq!(view.stride(2), 5);
    617
            assert_eq!(view.stride(3), 1);
    618
        }
    619
    620
        // cuda-tile-behavior.md: Falsification test #35 - Contiguity detection
    621
        #[test]
    622
        fn test_contiguity_after_operations() {
    623
            let view = TensorView::<f32>::new([4, 4, 1, 1]);
    624
            assert!(view.is_contiguous());
    625
    626
            // Slice preserves contiguity
    627
            let sliced = view.slice(1..3);
    628
            assert!(sliced.is_contiguous());
    629
    630
            // Transpose breaks contiguity
    631
            let transposed = view.transpose(0, 1);
    632
            assert!(!transposed.is_contiguous());
    633
        }
    634
    }