Skip to content
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

cfg features for enum variants #4509

Merged
merged 8 commits into from
Sep 16, 2024
Merged

Conversation

jeff-k
Copy link
Contributor

@jeff-k jeff-k commented Sep 1, 2024

This is a fix for #4411. #[cfg] attributes are propagated to the generated python classes as described in this comment: #4411 (comment)

It only covers the case for simple enums but a similar solution could be extended for complex enums.

Copy link
Member

@mejrs mejrs left a comment

Choose a reason for hiding this comment

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

Thanks!

I like the implementation, I just have a couple of points that need addressing:

  1. This PR uses attrs as variable and field names. Existing code uses cfg_attrs. This code should too, for consistency.
  2. Please add a test to ensure that something like
#[pyclass]
enum Empty{
    #[cfg(any())]
    Disabled,
}

does not compile. You can add it to tests/ui/invalid_pyclass_enum.rs

@jeff-k
Copy link
Contributor Author

jeff-k commented Sep 1, 2024

So in the case where all of the variants get disabled, like

#[pyclass]
enum Empty{
    #[cfg(any())]
    Disabled,
}

it does fail to compile, but not with the nice error: #[pyclass] can't be used on enums without any variants message.

Instead we get:

error[E0004]: non-exhaustive patterns: type `&Empty` is non-empty
  --> src/lib.rs:27:1
   |
27 | #[pyclass]
   | ^^^^^^^^^^
   |
note: `Empty` defined here
  --> src/lib.rs:28:6
   |
28 | enum Empty {
   |      ^^^^^
   = note: the matched value is of type `&Empty`
   = note: references are always considered inhabited
   = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info)
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern as shown
   |
27 ~ #[pyclass] {
28 +     _ => todo!(),
29 + }
   |

For more information about this error, try `rustc --explain E0004`.

Which I suppose is related to the reason why we have the first error message but I don't think we can know at this time which features are enabled and can't test for empty enums from disabled variants.

The error message we do get is because the 2 generated match arms in impl_simple_enum wind up empty. For fun I added a wildcard arm:

            fn __pyo3__repr__(&self) -> &'static str {
                match self {
                    #(#variants_repr)*
                    _ => unreachable!(),
                }
            }

and the generated code does compile but I don't think that could lead to a helpful way to report an invalid enum. The resulting python module silently does not have the empty enum class.

I'm not sure what to do. I could see a user defining an enum where every variant is covered by a cfg attribute and triggering this. It will still fail to compile but for the wrong reason so I'm not sure how critical this is.

I suppose we could work out the annihilating combination of variant cfg attributes and if they're met trigger a compile error?

For example, with an enum like

enum MyEnum {
    #[cfg(feature = "x")]
    VariantOne,
    #[cfg(feature = "y")]
    VariantTwo,
}

collecting the cfg features and generating this:

#[cfg(all(not(feature = "x"), not(feature = "y")))]
compile_error!("All variants of enum MyEnum have been disabled by cfg attributes");

Or is this just an edge case.

@mejrs
Copy link
Member

mejrs commented Sep 2, 2024

The error message we do get is because the 2 generated match arms in impl_simple_enum wind up empty. For fun I added a wildcard arm:

            fn __pyo3__repr__(&self) -> &'static str {
                match self {
                    #(#variants_repr)*
                    _ => unreachable!(),
                }
            }

and the generated code does compile but I don't think that could lead to a helpful way to report an invalid enum.

In this case, just dereference the scrutinee - maybe we should do this to avoid emitting this particular error

            fn __pyo3__repr__(&self) -> &'static str {
                match *self {
                    #(#variants_repr)*
                }
            }

Anyway, we should not rely on E0004. The rules around references to uninhabited types are still being worked out and such code may start compiling in the future.

I suppose we could work out the annihilating combination of variant cfg attributes and if they're met trigger a compile error?

Yes, I think this is a good idea.

Or is this just an edge case.

I can believe that someone would hit this eventually and waste a lot of time debugging ;)

Maybe we could reconsider allowing empty enums. I wasn't involved with that feature so I'm not sure there is a compelling reason to disallow it.

@jeff-k
Copy link
Contributor Author

jeff-k commented Sep 10, 2024

I've pushed a draft solution that constructs a #variant_cfg_check token stream that's inserted at the top of the generated class code.

If there is at least one variant that is not annotated with a cfg attribute or if there are zero variants then this token stream is quote! {}. Otherwise:

    quote! {
        #[cfg(all(#(#conditions),*))]
        ::core::compile_error!(concat!("All variants of enum `", stringify!(#cls), "` have been disabled by cfg attributes"));
    }

Here's an example:

#[pyclass(eq, eq_int)]
#[derive(PartialEq)]
enum MyEnum {
    #[cfg(not(any(feature = "x", feature="y")))]
    VariantOne,
    #[cfg(all(feature = "y", feature="optional_feature"))]
    VariantTwo,
}
$ cargo build --features y
...
error: All variants of enum `MyEnum` have been disabled by cfg attributes
  --> src/lib.rs:32:1
   |
32 | #[pyclass(eq, eq_int)]
   | ^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info)

I've also dereferenced the scrutinees so that the body of the generated class is syntactically valid and the E0004 doesn't also occur. I haven't investigated why I don't see an unreachable code warning but I think in theory we are liable to get one, as happens in this example:

warning: unreachable statement
  --> src/lib.rs:68:5
   |
67 |     let _y = match *uninhabited { };
   |              ---------------------- any code following this expression is unreachable
68 |     println!("unreachable");
   |     ^^^^^^^^^^^^^^^^^^^^^^^ unreachable statement
   |
   = note: `#[warn(unreachable_code)]` on by default
   = note: this warning originates in the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

I don't have any input on what adding unconstructable types to python bindings would look like. I think python's typing.Never was added in 3.11 so I suppose there's at least a notion of it.

Is this style consistent with pyo3?

I will continue with making tests

@jeff-k jeff-k requested a review from mejrs September 14, 2024 15:40
let cfg_attrs = &variant.cfg_attrs;

if cfg_attrs.is_empty() {
return quote! {};
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return quote! {};
// There's at least one variant of the enum without cfg attributes,
// so the check is not necessary
return quote! {};

@@ -66,6 +66,14 @@ error: The `ord` option requires the `eq` option.
83 | #[pyclass(ord)]
| ^^^

error: All variants of enum `AllEnumVariantsDisabled` have been disabled by cfg attributes
Copy link
Member

Choose a reason for hiding this comment

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

We should probably mention why it's not allowed:

Suggested change
error: All variants of enum `AllEnumVariantsDisabled` have been disabled by cfg attributes
error: #[pyclass] can't be used on enums without any variants - all variants of enum `AllEnumVariantsDisabled` have been configured out by cfg attributes

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, good. I see we don't put #[pyclass] in backticks elsewhere in this module's compiler errors, too, so I haven't added them.

Comment on lines 72 to 73
96 | #[pyclass(eq)]
| ^^^^^^^^^^^^^^
Copy link
Member

Choose a reason for hiding this comment

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

It would be nice to have a better span for this error, cls.span() should do.

@jeff-k jeff-k requested a review from mejrs September 14, 2024 23:17
Copy link
Member

@mejrs mejrs left a comment

Choose a reason for hiding this comment

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

Thanks!

@mejrs mejrs added this pull request to the merge queue Sep 16, 2024
Merged via the queue into PyO3:main with commit a32afdd Sep 16, 2024
43 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants