Skip to content

Add multislice! macro #388

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Dec 9, 2018
Merged

Conversation

jturner314
Copy link
Member

@jturner314 jturner314 commented Nov 29, 2017

This PR adds a multislice!() macro which makes it possible to take multiple simultaneous mutable (and read-only) slices of an array. It panics at runtime if slices violate Rust's aliasing rules.

Regarding the implementation:

  • It's a lot of new lines of code, but most of the new lines are comments, docs, and tests.

  • Most of the new functions are in dimension/mod.rs. They could also be put in slice.rs, whatever makes more sense.

  • Additions to the public API are hidden from the docs (except for multislice!()).

  • Basically, the way multislice!() works is to borrow the array a single time by calling view_mut(), and then copy the borrow as many times as needed to create the resulting ArrayView and ArrayViewMut instances. This ensures that the underlying array is mutably borrowed and all the lifetimes are correct.

    I didn't see a good way to do that with the existing public API, so I added two (hidden) methods to ArrayViewMut: aliasing_view() and aliasing_view_mut().

    The primary disadvantage of this approach is that it always mutably borrows the array, even if there aren't any mut slices, but that's not a big issue because if you aren't taking any mut slices, you can just call .slice() multiple times anyway.

  • This is the first time I've used quickcheck, and I love it! I caught a few bugs in my implementation on edge cases with quickcheck tests.

Please let me know what you think!

Edit: Would it be better to return an Err instead of panicking if the slices violate the aliasing rules?

@bluss bluss self-assigned this Nov 29, 2017
@jturner314 jturner314 force-pushed the multi-slice branch 2 times, most recently from c136b72 to 8b862be Compare December 7, 2017 19:19
@bluss
Copy link
Member

bluss commented Dec 31, 2017

Very well written PR!

@bluss
Copy link
Member

bluss commented Jan 11, 2018

Do you want me to look into the Raw pointer array view?

@jturner314
Copy link
Member Author

Do you want me to look into the Raw pointer array view?

That would be great, thanks. I have a start on the implementation in jturner314/rust-ndarray/dataraw-trait but I don't have much time to work on it at the moment.

@jturner314
Copy link
Member Author

I've updated the PR to use the RawArrayViewMut type so that it's no longer necessary to temporarily create mutable aliasing views.

Now (using s_a, s_b, s_c, and s_d variables to simplify the expansion),

let (a, b, c, d) = multislice!(arr, (mut s_a, s_b, mut s_c, s_d));

expands to the following (generated with cargo +nightly rustc --example foo -- -Z unstable-options --pretty=expanded,hygiene and manually un-expanding the assert! calls):

let (a /* 107#0 */, b /* 108#0 */, c /* 109#0 */, d /* 110#0 */) =
    {
        let (life /* 11149#22 */, raw_view /* 11021#22 */) =
            {
                let mut view /* 10978#22 */ =
                    ::ArrayBase /* 10959#22 */::view_mut /* 10979#22
                        */(&mut arr /* 93#0 */);
                (::life_of_view_mut /* 10783#22 */(&view /* 10978#22 */),
                 view /* 10978#22 */.raw_view_mut /* 11022#22 */())
            };
        match s_a /* 100#0 */ {
            info /* 11259#23 */ => {
                match s_b /* 103#0 */ {
                    info /* 11259#24 */ => {
                        assert!(!::slices_intersect /* 10954#25
                                 */(&raw_view /* 11021#22 */.raw_dim /*
                                         10799#25 */(),
                                    info /* 11259#24 */,
                                    info /* 11259#23 */));
                        match s_c /* 104#0 */ {
                            info /* 11259#36 */ => {
                                assert!(!::slices_intersect /* 10954#37
                                         */(&raw_view /* 11021#22
                                                 */.raw_dim /* 10799#37
                                                 */(),
                                            info /* 11259#36 */,
                                            info /* 11259#23 */));
                                assert!(!::slices_intersect /* 10954#48
                                         */(&raw_view /* 11021#22
                                                 */.raw_dim /* 10799#48
                                                 */(),
                                            info /* 11259#36 */,
                                            info /* 11259#24 */));
                                match s_d /* 106#0 */ {
                                    info /* 11259#60 */ => {
                                        {
                                            assert!(!::slices_intersect /*
                                                     10954#62
                                                     */(&raw_view /*
                                                             11021#22
                                                             */.raw_dim /*
                                                             10799#62
                                                             */(),
                                                        info /* 11259#60
                                                            */,
                                                        info /* 11259#23
                                                            */));
                                            assert!(!::slices_intersect /*
                                                     10954#73
                                                     */(&raw_view /*
                                                             11021#22
                                                             */.raw_dim /*
                                                             10799#73
                                                             */(),
                                                        info /* 11259#60
                                                            */,
                                                        info /* 11259#36
                                                            */));
                                        };
                                        (#[allow(unsafe_code)] unsafe {
                                                                   ::deref_raw_view_mut_into_view_mut_with_life
                                                                       /*
                                                                       10785#84
                                                                       */(raw_view
                                                                              /*
                                                                              11021#22
                                                                              */.clone
                                                                              /*
                                                                              474#84
                                                                              */().slice_move
                                                                              /*
                                                                              10983#84
                                                                              */(info
                                                                                     /*
                                                                                     11259#23
                                                                                     */),
                                                                          life
                                                                              /*
                                                                              11149#22
                                                                              */)
                                                               },
                                         #[allow(unsafe_code)] unsafe {
                                                                   ::deref_raw_view_mut_into_view_with_life
                                                                       /*
                                                                       10784#85
                                                                       */(raw_view
                                                                              /*
                                                                              11021#22
                                                                              */.clone
                                                                              /*
                                                                              474#85
                                                                              */().slice_move
                                                                              /*
                                                                              10983#85
                                                                              */(info
                                                                                     /*
                                                                                     11259#24
                                                                                     */),
                                                                          life
                                                                              /*
                                                                              11149#22
                                                                              */)
                                                               },
                                         #[allow(unsafe_code)] unsafe {
                                                                   ::deref_raw_view_mut_into_view_mut_with_life
                                                                       /*
                                                                       10785#86
                                                                       */(raw_view
                                                                              /*
                                                                              11021#22
                                                                              */.clone
                                                                              /*
                                                                              474#86
                                                                              */().slice_move
                                                                              /*
                                                                              10983#86
                                                                              */(info
                                                                                     /*
                                                                                     11259#36
                                                                                     */),
                                                                          life
                                                                              /*
                                                                              11149#22
                                                                              */)
                                                               },
                                         #[allow(unsafe_code)] unsafe {
                                                                   ::deref_raw_view_mut_into_view_with_life
                                                                       /*
                                                                       10784#87
                                                                       */(raw_view
                                                                              /*
                                                                              11021#22
                                                                              */.clone
                                                                              /*
                                                                              474#87
                                                                              */().slice_move
                                                                              /*
                                                                              10983#87
                                                                              */(info
                                                                                     /*
                                                                                     11259#60
                                                                                     */),
                                                                          life
                                                                              /*
                                                                              11149#22
                                                                              */)
                                                               })
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    };

I'd like to add a few more tests, and I'm strongly considering returning an error instead of panicking when the slices violate the aliasing rules, but otherwise, this PR is mostly done.

Any thoughts on the error vs. panic question?

src/slice.rs Outdated
/// disjoint).
///
/// The syntax is `multislice!(` *expression, (pattern [, pattern [, …]])* `)`,
/// where *expression* evaluates to an `ArrayBase<S, D>` where `S: DataMut`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you disagree (since you wrote it this way), but I'd prefer to say "mutable array" rather than "ArrayBase<S, D> where S: DataMut". (Yes I'm constantly unhappy with exposing ArrayBase, DataMut like this 🙂 .)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like "mutable array" better too. I think I originally wrote it that way because macros don't have type signatures like methods do, which tends to make me want to be fairly explicit about the types they expect and return. In this case, though, "mutable array" is clear and less verbose.

@bluss
Copy link
Member

bluss commented Dec 2, 2018

I'll have to get back to reviewing this (hopefully soon), but amazing work! I would first think that indexing errors should, “as usual”, result in a panic. If it's an error -- what would the user do with it?

/// let j = arr2(&[[1, 3],
/// [5, 7]]);
/// assert_eq!(s0, i);
/// assert_eq!(s1, j);
Copy link
Member

@bluss bluss Dec 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 🔥 🔥 🔥

(Just the feature itself is really nice.)

src/slice.rs Outdated
///
/// # fn main() {
/// let mut arr = Array1::from_iter(0..12);
/// let (a, b, c, d) = multislice!(arr, (s![0..5], mut s![6..;2], s![1..6], mut s![7..;2]));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nested macros -- s![] inside multislice seem natural enough. But we and others will ask.. is that extra s! thing necessary, this is already a macro?

But of course, there is not much that can be simplified, we need some kind of border to group each slice, and the s! syntax is at least already familiar.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. I wrote it that way because it was the most obvious way to allow the user to supply a &SliceInfo instance instead of always requiring them to use s!-style notation. However, it would be possible to use pattern-matching to differentiate between s!-style notation and an expression that evaluates to &SliceInfo, so the s! isn't strictly necessary. That said, I think that we should keep it as-is for the following reasons:

  • It's easier to read multislice!(arr, (s![0..5], mut s![6..;2])) at a glance than multislice!(arr, ([0..5], mut [6..;2])) if you have syntax highlighting because the s! is shown in a different color, which makes it really easy to see the grouping of the slicing info.
  • When the user writes multislice!(arr, (s![0..5], mut s![6..;2])), it's clear what the s! means because the s! macro is used fairly often. Without the s!, it's not quite as obvious.
  • The multislice! implementation is a little simpler without having to provide special parsing for s!-style expressions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious, is it technically possible to use this syntax: multislice!(arr, [0..5], mut [6..;2])?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes; we can just add rules that apply the s![] macro, matching on [$($info:tt)*], above the rules matching on $info:expr:

diff --git a/src/slice.rs b/src/slice.rs
index 0d358f1..a06a8fd 100644
--- a/src/slice.rs
+++ b/src/slice.rs
@@ -770,6 +770,108 @@ macro_rules! multislice(
             )
         }
     };
+    // Parse last slice (mutable), no trailing comma, applying `s![]` macro.
+    (
+        @parse $view:expr, $life:expr,
+        ($($sliced:tt)*),
+        ($($mut_info:tt)*),
+        ($($immut_info:tt)*),
+        (mut [$($info:tt)*])
+    ) => {
+        // Apply `s![]` macro to info.
+        $crate::multislice!(
+            @parse $view, $life,
+            ($($sliced)*),
+            ($($mut_info)*),
+            ($($immut_info)*),
+            (mut s![$($info)*],)
+        )
+    };
+    // Parse last slice (read-only), no trailing comma, applying `s![]` macro.
+    (
+        @parse $view:expr, $life:expr,
+        ($($sliced:tt)*),
+        ($($mut_info:tt)*),
+        ($($immut_info:tt)*),
+        ([$($info:tt)*])
+    ) => {
+        // Apply `s![]` macro to info.
+        $crate::multislice!(
+            @parse $view, $life,
+            ($($sliced)*),
+            ($($mut_info)*),
+            ($($immut_info)*),
+            (s![$($info)*],)
+        )
+    };
+    // Parse last slice (mutable), with trailing comma, applying `s![]` macro.
+    (
+        @parse $view:expr, $life:expr,
+        ($($sliced:tt)*),
+        ($($mut_info:tt)*),
+        ($($immut_info:tt)*),
+        (mut [$($info:tt)*],)
+    ) => {
+        // Apply `s![]` macro to info.
+        $crate::multislice!(
+            @parse $view, $life,
+            ($($sliced)*),
+            ($($mut_info)*),
+            ($($immut_info)*),
+            (mut s![$($info)*],)
+        )
+    };
+    // Parse last slice (read-only), with trailing comma, applying `s![]` macro.
+    (
+        @parse $view:expr, $life:expr,
+        ($($sliced:tt)*),
+        ($($mut_info:tt)*),
+        ($($immut_info:tt)*),
+        ([$($info:tt)*],)
+    ) => {
+        // Apply `s![]` macro to info.
+        $crate::multislice!(
+            @parse $view, $life,
+            ($($sliced)*),
+            ($($mut_info)*),
+            ($($immut_info)*),
+            (s![$($info)*],)
+        )
+    };
+    // Parse a mutable slice, applying `s![]` macro.
+    (
+        @parse $view:expr, $life:expr,
+        ($($sliced:tt)*),
+        ($($mut_info:tt)*),
+        ($($immut_info:tt)*),
+        (mut [$($info:tt)*], $($t:tt)*)
+    ) => {
+        // Apply `s![]` macro to info.
+        $crate::multislice!(
+            @parse $view, $life,
+            ($($sliced)*),
+            ($($mut_info)*),
+            ($($immut_info)*),
+            (mut s![$($info)*], $($t)*)
+        )
+    };
+    // Parse a read-only slice, applying `s![]` macro.
+    (
+        @parse $view:expr, $life:expr,
+        ($($sliced:tt)*),
+        ($($mut_info:tt)*),
+        ($($immut_info:tt)*),
+        ([$($info:tt)*], $($t:tt)*)
+    ) => {
+        // Apply `s![]` macro to info.
+        $crate::multislice!(
+            @parse $view, $life,
+            ($($sliced)*),
+            ($($mut_info)*),
+            ($($immut_info)*),
+            (s![$($info)*], $($t)*)
+        )
+    };
     // Parse last slice (mutable), no trailing comma.
     (
         @parse $view:expr, $life:expr,

This change would be backwards compatible if we want to add it in the future.

Copy link
Member

@bluss bluss Dec 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just counting in syntactic noise, it seems like a big win to drop both the extra () around the slices and each s!, I would go for that right away. The s! would keep working, because you've designed it so we can insert arbitrary expressions for each axis output, right? You could do (?)

let ax1 = s![1..];
let ax2 = s![2..];
multislice!(arr, ax1, ax2);

I guess it would count as us breaking array expressions (like [1,2,3]) in those locations, but there is no reason to use those there at this point.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I didn't notice the dropped () in your earlier comment. Yeah, we can drop the () too. With the extra () dropped, I think it's fine to drop the s! too. (With the (), it looks like a tuple, which makes me want to require the extra s! so that the elements are expressions. Without the (), the parameters are clearly parameters to the macro and not part of a tuple, so IMO it's fine to do things like interpreting them as s!-style syntax without requiring the s!.)

I'll update the PR to drop the extra () and s!.

The s! would keep working, because you've designed it so we can insert arbitrary expressions for each output, right?

Yes, normal expressions that evaluate to &SliceInfo will still work. (They just have a lower precedence than [$($info:tt)*].)

I guess it would count as us breaking array expressions (like [1,2,3]) in those locations, but there is no reason to use those there at this point.

Yeah, these don't matter right now, because the slicing expressions must have type &SliceInfo, and an expression of the form [$($info:tt)*] is always a fixed-length array, not a &SliceInfo. If we want to add support for slicing using fixed-length array expressions in the future (e.g. [1, 2, 3] and [0..5, 3..7, 2..8] for Ix3) that don't use the s! macro machinery, we can do so by adding more rules with even higher precedence. (They'd match on e.g. [$($info:expr),*].)

One interesting thing to note is that we could implement multislice! as a method on ArrayBase, instead of a macro, that could be called like this:

arr.multislice((s![0..5], Mut(s![6..;2])))
// or
arr.multislice(ss!([0..5], mut [6..;2]))

but that would work up to only a limited tuple length (however long we'd decide to support), like Zip works up to only 6 producers.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a method sounds cool but the syntax doesn't work quite so well there, so it doesn't seem as good. Hopefully multislice as a macro has its own attraction by virtue of its functionality so that users find it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually think arr.multislice(ss!([0..5], mut [6..;2])) is quite nice, but it would be more complex to implement than the multislice! macro.

I've updated the PR to drop the extra () and s!.

@jturner314
Copy link
Member Author

I would first think that indexing errors should, “as usual”, result in a panic.

Out-of-bounds indices should panic, but there isn't precedent for how to handle overlapping slices, so we could go either way. The primary issue with panicking is that determining whether slices will overlap for arbitrary indices is somewhat tricky, and I like to keep panic conditions simple and easy-to-avoid. Additionally, if we return an error, the user can easily convert it into a panic with .unwrap() or .expect().

If it's an error -- what would the user do with it?

That's a really good question. If there is an overlap, is there a feasible way to recover? I have trouble coming up with a realistic case where you'd want to do anything other than panic.

I've been using multislice! (the old, questionable aliasing_view/aliasing_view_mut implementation) for a while in my own code. For my use cases, the way I'm using it always makes it obvious that the slices won't overlap. I don't think tricky cases where it's difficult to determine whether the slices might overlap would actually come up in practice.

So, on further thought, I think that multislice! should panic instead of returning an error. If necessary, we can expose the slices_intersect functionality to allow the user to easily determine if the macro will panic or not before calling it.

@bluss
Copy link
Member

bluss commented Dec 9, 2018

This is ready to go, right? Everything looks good to me! Then we can soon release 0.13 with this and updated matrixmultiply

@jturner314 jturner314 merged commit 5d2f7f6 into rust-ndarray:master Dec 9, 2018
@jturner314 jturner314 deleted the multi-slice branch December 9, 2018 22:49
@jturner314
Copy link
Member Author

I looked over the PR again and merged it. I'm glad to get this merged; I'll finally be able to use mainline ndarray again!

Then we can soon release 0.13 with this and updated matrixmultiply

I have a couple more breaking changes I'd like to make in order to support more slicing operations. I'll submit the PR today.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants