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

Fix dead_code warning when returning Result with bridged type in error case #278

Merged
merged 4 commits into from
Jun 21, 2024

Conversation

sax
Copy link
Contributor

@sax sax commented Jun 17, 2024

In Rust 1.79.0, dead code warnings are emitted from enums where the compiler does not infer that wrapped data is used, even when the enum has been annotated with #[repr(C]. This warning triggers in swift-bridge when defining transparent structs or enums that will be returned in error results.

This appears to be a regression in the dead_code rustc lint. Pending upstream fixes to rustc, this change to swift-bridge annotates generated result enums with #[allow(unused)].

Fixes #270

error: field `0` is never read
   |
1  | #[swift_bridge::bridge]
   | ----------------------- field in this variant
...
3  |     enum ResultTransparentEnum {
   |          ^^^^^^^^^^^^^^^^^^^^^
   |
help: consider changing the field to be of unit type to suppress this warning while preserving the field numbering, or remove the field
   |
3  |     enum () {
   |          ~~

Example bridge

The following bridge code is the minimal reproduction case:

#[swift_bridge::bridge]
mod ffi {
    #[swift_bridge(swift_repr = "struct")]
    struct TransparentErrorStruct(pub String);

    extern "Rust" {
        fn rust_func_returns_result_transparent_struct(
            succeed: bool
        ) -> Result<(), TransparentErrorStruct>;
    }
}

fn rust_func_returns_result_transparent_struct(
    succeed: bool
) -> Result<(), ffi::ResultTestTransparentStruct> {
    if succeed {
        Ok(())
    } else {
        Err(ffi::ResultTestTransparentStruct("failed".to_string()))
    }
}

impl std::error::Error for ffi:: TransparentErrorStruct {}

impl std::fmt::Debug for ffi:: TransparentErrorStruct {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self)
    }
}

impl std::fmt::Display for ffi:: TransparentErrorStruct {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

@chinedufn
Copy link
Owner

chinedufn commented Jun 18, 2024

Thank you for tracking this down, figuring out a fix, and submitting a PR. I appreciate you taking the time for this.

I have a few small requests to be able to land this:


Can you update your PR body with:

  • an example bridge module that would have left to a warning before this commit
  • an exampe of how the warning message looks

All PRs (PR body gets squashed into the commit message) should include an example of what they now enable/fix.
We don't want contributors to have to click through links or read the diff to get a basic understanding of what is being enabled.

We use the example in the PR body when:

  • writing release notes
  • linking new contributors to older similar PRs

So it's nice if they can be understood at a glance.


I looked at the Announcing Rust 1.79.0 post and I don't see anything about changes to dead code lints. Where did you find that it was introduced in 1.79.0?

I ask because I want to understand whether this is a regression or an intentional change in rustc.


We are running 1.79.0 in CI, yet CI is passing. Here's a run from 3 days ago.

https://github.com/chinedufn/swift-bridge/actions/runs/9532352837/job/26274367654

image

In CI we deny warnings:

- name: Run tests
run: |
RUSTFLAGS="-D warnings" cargo test -p swift-bridge \

We'll want to ensure that the test suite fails without the #[allow(unused)] that was introduced in this commit.
This would make it possible for us to remove it in the future and be sure that we haven't re-introduced the warning.

We can modify the crates/swift-integration-tests/src/result.rs such that we get this warning without the #[allow(unused)].


All unusual code should be documented. If no one knows why the #[allow(unused)] was added they'll be afraid to try to remove it.

Can document it here

pub fn generate_custom_rust_ffi_types(
.

OR can document it on/near the test case. Or mention in both places. Whatever you think makes the most sense.

A contributor should not need to click the link to get a basic understanding of the problem/solution, so any links should be supplemental.
litmus test: "could a contributor understand this documentation/explanation if they were offline".


Finally, why would the Err variant need #[allow(unused)], but not the Ok variant?
That should be made clear in:

  • the commit message
  • near the test case and/or near the implementation code

@sax
Copy link
Contributor Author

sax commented Jun 19, 2024

Sure thing and thanks for the feedback. All of this makes sense.

I don't 100% understand why the Err case generates a warning but the Ok does not. I'll see if I can figure out why this only started happening with 1.79.0. I definitely see issues and pull requests on other projects that have started facing this issue over the past few months, and in some cases having to #[allow(unused)] across dozens of enums across their codebases. But that still doesn't explain why it started here with 1.79.0.

@sax sax force-pushed the fix-result-dead-code-warning branch from cc562e4 to 327d02a Compare June 19, 2024 07:54
@sax sax marked this pull request as draft June 19, 2024 16:37
@sax
Copy link
Contributor Author

sax commented Jun 19, 2024

I can't find the specific change that introduced this in the rust linters, but I see issues around this going back to 2021: rust-lang/rust#88900. More recently there are rust-lang/rust#123068 and rust-lang/rust#123418.

My supposition is that even though the lints go back multiple years, until recently they were hidden when generated from proc macros. I'll keep looking.

@sax
Copy link
Contributor Author

sax commented Jun 19, 2024

Maybe it's this: rust-lang/rust#120845. -Z debug-macros was stabilized and enabled by default. I see in a comment here (rust-lang/rust#100758 (comment)) that #[collapse_debuginfo] is not supported for procedural macros, which might be why for swift-bridge the warning is terse and just points to the definition of the bridged error struct without any information about the generated result enum.

@sax sax force-pushed the fix-result-dead-code-warning branch from 327d02a to 879d9ca Compare June 19, 2024 17:36
@chinedufn
Copy link
Owner

chinedufn commented Jun 19, 2024

Code generation happens before the linting pass so it's unlikely to be related to proc macros.

I took a look at some of the recent changes to the dead code lint https://github.com/rust-lang/rust/commits/master/compiler/rustc_passes/src/dead.rs?before=894f7a4ba6554d3797404bbf550d9919df060b97+35

Candidate 1

This one stands out: rust-lang/rust#119545

Here's the commit that closes that issue rust-lang/rust@a0fe413

And here's the PR ( I haven't looked at the PR, just linking for reference rust-lang/rust#119552 )

Note this ui test in the commit linked above:

#![deny(dead_code)]

fn main() {
    let _ = foo::S{f: false};
}

mod foo {
    pub struct S {
        pub f: bool, //~ ERROR field `f` is never read
    }
}
error: field `f` is never read
  --> $DIR/pub-field-in-priv-mod.rs:9:13
   |
LL |     pub struct S {
   |                - field in this struct
LL |         pub f: bool,
   |             ^
   |
note: the lint level is defined here
  --> $DIR/pub-field-in-priv-mod.rs:1:9
   |
LL | #![deny(dead_code)]
   |         ^^^^^^^^^

error: aborting due to 1 previous error

Same error as the one you reported in #270


So, from a quick scan, I'm thinking:

  • BEFORE: pub enum inside of a private module would not get a dead code lint
  • AFTER: pub enum inside a private module gets a dead code lint if rustc does not think that it is used

Candidate 2

This commit makes unused tuple struct fields a warning, instead of allowed rust-lang/rust@9fcf9c1

Conclusion

Seems like the issue might potentially be a combination of Candidate 1 and Candidate 2... Or maybe something else .. not sure ... I'd have to bisect ...

Regardless, the generated enum FFI representations that this PR currently modifies all have #[repr(C)] on them, which I would expect the compiler to treat as used.

Looks like there was a recent discussion about this: rust-lang/rust#119659 (comment) rust-lang/rust#119659 (comment)

Also, when you add the fact that the enum is used, this seems like a regression in rustc:

#[repr(C)]
pub enum ResultSomeOkTypeAndSomeErrEnum{
Ok(*mut super::SomeOkType),
Err(__swift_bridge__SomeErrEnum),
}
#[export_name = "__swift_bridge__$some_function"]
pub extern "C" fn __swift_bridge__some_function() -> ResultSomeOkTypeAndSomeErrEnum{
match super::some_function() {
Ok(ok) => ResultSomeOkTypeAndSomeErrEnum::Ok(Box::into_raw(Box::new({
let val: super::SomeOkType = ok;
val
})) as *mut super::SomeOkType),
Err(err) => ResultSomeOkTypeAndSomeErrEnum::Err(err.into_ffi_repr()),
}
}

Note that the pub extern "C" fn __swift_bridge__some_function that I linked to above is making use of the pub enum ResultSomeOkTypeAndSomeErrEnum.

So I think the real fix here lies in rustc, not swift-bridge.

@chinedufn
Copy link
Owner

Heres a minimal example that generates the warning in rust 1.79.0

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=3f5c81c0749130a6e46fbea14cbb055f

fn main() {}

mod private_mod {
    #[repr(C)]
    pub enum MyEnum {
        Variant(u32)
    }
    
    #[no_mangle]
    pub extern "C" fn some_function() -> MyEnum {
        MyEnum::Variant(5)
    }
}
   Compiling playground v0.0.1 (/playground)
warning: field `0` is never read
 --> src/main.rs:6:17
  |
6 |         Variant(u32)
  |         ------- ^^^
  |         |
  |         field in this variant
  |
  = note: `#[warn(dead_code)]` on by default
help: consider changing the field to be of unit type to suppress this warning while preserving the field numbering, or remove the field
  |
6 |         Variant(())
  |                 ~~

warning: `playground` (bin "playground") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.85s
     Running `target/debug/playground`

Notice that the warning is incorrect since the enum variant is being used.

This is a regression in rustc's dead code pass ( this file -> https://github.com/rust-lang/rust/commits/master/compiler/rustc_passes/src/dead.rs?before=894f7a4ba6554d3797404bbf550d9919df060b97+35 ).

@sax sax force-pushed the fix-result-dead-code-warning branch from 879d9ca to a9530a2 Compare June 19, 2024 17:53
@sax
Copy link
Contributor Author

sax commented Jun 19, 2024

Does this mean you don't think this swift-bridge should #[allow(unused)], even pending a fix in rustc? I'm finding that when running the swift-bridge tests while removing the #![allow(dead_code)] from the result integration test, warnings are generated for both transparent enums and structs.

When I allow unused just for the Err variant, transparent structs pass through without warnings (so the Ok variant is somehow ok). When I allow unused for both Ok and Err, then the transparent enums also succeed without warnings.

@chinedufn
Copy link
Owner

chinedufn commented Jun 19, 2024

I opened an issue here: rust-lang/rust#126706

Temporarily adding #[allow(unused)] is ok as long as:

  1. we introduce a failing test case (I think you already did in a recent commit, but I haven't read it yet)

  2. we mention and link to the issue in a way that makes it easy for a future maintainer to realize that we can delete the #[allow(unused)] when the issue is closed.

Let me know if you disagree or have another plan in mind.

EDIT fixed the issue link

@sax
Copy link
Contributor Author

sax commented Jun 19, 2024

Did you mean to open that on rustc? Looks like you opened it on swift-bridge.

I have a new test for transparent structs, and removed an allowance from the integration tests so that removing the #[allow(unused)] reintroduces the warning on transparent enums.

I'll work on adding a minimal bridge reproduction case to the description of this PR, and add a TODO to the code once I'm totally sure which issue to link to.

Thanks for the taking the time to look at all the commits on rust. With all the recent issues related to these sorts of warnings, I had assumed that this was an intentional change to the linter.

@sax sax force-pushed the fix-result-dead-code-warning branch from a9530a2 to 0fd3075 Compare June 19, 2024 18:39
@sax sax marked this pull request as ready for review June 19, 2024 18:40
@sax
Copy link
Contributor Author

sax commented Jun 19, 2024

Please take a look again at your convenience! After understanding that the PR description gets squashed onto the merged commit, I added more context to the PR itself and made the text on the commit itself more terse.

One thing to note is that in the code I'm linking to #279 on swift-bridge, but if you were intending that to be an issue on rustc I can change it (or you can modify this PR to save time) link to a different issue.

@chinedufn
Copy link
Owner

Just fixed the link -> rust-lang/rust#126706

Great, I'll review today or tomorrow.

@sax sax force-pushed the fix-result-dead-code-warning branch from 0fd3075 to d63b7ba Compare June 19, 2024 21:44
Copy link
Owner

@chinedufn chinedufn left a comment

Choose a reason for hiding this comment

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

Code looks great.

Let some minor requests around making it easier for a maintainer from 5 years from now to understand all of this.

I left notes about what information to include, so hopefully you can just take that, massage it, and then plop it into the comments.

After that this looks good to me and we can land it.

Thanks again for researching and solving this. This is our first time papering over a Rust regression so it was nice to iron out how we should do this if it ever happens again.
Good idea regarding landing the #[allow(unused)] for now and then removing it in the future vs. my original idea of just closing this out.

Cheers.

@@ -375,10 +375,14 @@ impl BuiltInResult {
.err_ty
.to_ffi_compatible_rust_type(swift_bridge_path, types);
let mut custom_rust_ffi_types = vec![];
// TODO: remove allowances when rustc no longer issues dead code warnings for `#[repr(C)]`
Copy link
Owner

Choose a reason for hiding this comment

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

Let's explicitly say "#[allow(unused]" instead of "allowances".

Usually I'd say this is a minor nit that you could feel free to ignore, but because the codegen is fairly complex it's nice to leave as little room for misinterpretation as possible so let's make this change.

@@ -407,11 +407,15 @@ mod extern_rust_fn_return_result_opaque_rust_type_and_transparent_enum_type {
}
}

// In Rust 1.79.0 dead_code warnings are issued for wrapped data in enums in spite of the enum
// having `#[repr(C)]`.
Copy link
Owner

Choose a reason for hiding this comment

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

Let's:

  • link to the Rust issue
  • make it clear that this can be deleted when that issue is closed

This way no matter where you run into this #[allow(unused) you can easily check whether it can be removed.

@@ -484,11 +488,14 @@ mod extern_rust_fn_return_result_transparent_enum_type_and_opaque_rust_type {
}
}

// Allows unused to avoid dead_code warnings in Rust 1.79.0 or later.
Copy link
Owner

Choose a reason for hiding this comment

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

Let's:

  • link to the Rust issue
  • make it clear that this can be deleted when that issue is closed

This way no matter where you run into this #[allow(unused) you can easily check whether it can be removed.

@@ -558,11 +565,14 @@ mod extern_rust_fn_return_result_unit_type_and_transparent_enum_type {
}
}

// Allows unused to avoid dead_code warnings in Rust 1.79.0 or later.
Copy link
Owner

Choose a reason for hiding this comment

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

Let's:

  • link to the Rust issue
  • make it clear that this can be deleted when that issue is closed

This way no matter where you run into this #[allow(unused) you can easily check whether it can be removed.

@@ -628,12 +638,15 @@ mod extern_rust_fn_return_result_tuple_type_and_transparent_enum_type {
}
}

// Allows unused to avoid dead_code warnings in Rust 1.79.0 or later.
Copy link
Owner

Choose a reason for hiding this comment

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

Let's:

  • link to the Rust issue
  • make it clear that this can be deleted when that issue is closed

This way no matter where you run into this #[allow(unused) you can easily check whether it can be removed.

Comment on lines 40 to 47
#[swift_bridge(swift_repr = "struct")]
struct ResultTestTransparentStruct(pub String);

extern "Rust" {
fn rust_func_returns_result_null_transparent_struct(
succeed: bool,
) -> Result<(), ResultTestTransparentStruct>;
}
Copy link
Owner

Choose a reason for hiding this comment

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

As it stands now it's unclear why this is here. A maintainer 5 years from now would look at this and be confused as to why we aren't calling it from the Swift side.
This is here for a special reason (to prevent regressions when we remove this #[allow(unused)], but this will not be clear to a maintainer in 5 years)


Let's explain why this was put here.

  • In Rust 1.79.0 we noticed a warning when we returned -> Result<*, TransparentStruct> when the struct has at least one field
  • This is a regression in Rust that should be fixed in a future version
  • At some point after that regression is fixed we'll remove the #[allow(unused)] from the coegen
  • This test is meant to help us be sure that after removing the #[allow(unused)] the warning does not come back
  • We do not call this function from Swift since only exists here as a test that no warning is generated in Rust

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Funny thing... I didn't even see the Swift integration tests. I'll do one better and add a test for results with transparent structs there. It seems like I'm probably not the only person who will want to use that feature.

Copy link
Owner

Choose a reason for hiding this comment

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

That works, but if that's the case need to also add a codegen test for a Rust function that returns -> Result<TransparentStruct, TransparentStruct>

Similar to this but for transparent structs

/// Test code generation for Rust function that accepts a Result<T, E> where T and E are
/// opaque Rust types.
mod extern_rust_fn_return_result_opaque_rust {
use super::*;
fn bridge_module_tokens() -> TokenStream {
quote! {
mod ffi {
extern "Rust" {
type SomeType;
fn some_function () -> Result<SomeType, SomeType>;
}
}
}
}
fn expected_rust_tokens() -> ExpectedRustTokens {
ExpectedRustTokens::Contains(quote! {
#[export_name = "__swift_bridge__$some_function"]
pub extern "C" fn __swift_bridge__some_function() -> swift_bridge::result::ResultPtrAndPtr {
match super::some_function() {
Ok(ok) => {
swift_bridge::result::ResultPtrAndPtr {
is_ok: true,
ok_or_err: Box::into_raw(Box::new({
let val: super::SomeType = ok;
val
})) as *mut super::SomeType as *mut std::ffi::c_void
}
}
Err(err) => {
swift_bridge::result::ResultPtrAndPtr {
is_ok: false,
ok_or_err: Box::into_raw(Box::new({
let val: super::SomeType = err;
val
})) as *mut super::SomeType as *mut std::ffi::c_void
}
}
}
}
})
}
fn expected_swift_code() -> ExpectedSwiftCode {
ExpectedSwiftCode::ContainsAfterTrim(
r#"
public func some_function() throws -> SomeType {
try { let val = __swift_bridge__$some_function(); if val.is_ok { return SomeType(ptr: val.ok_or_err!) } else { throw SomeType(ptr: val.ok_or_err!) } }()
}
"#,
)
}
const EXPECTED_C_HEADER: ExpectedCHeader = ExpectedCHeader::ContainsAfterTrim(
r#"
struct __private__ResultPtrAndPtr __swift_bridge__$some_function(void);
"#,
);
#[test]
fn extern_rust_fn_return_result_opaque_rust() {
CodegenTest {
bridge_module: bridge_module_tokens().into(),
expected_rust_tokens: expected_rust_tokens(),
expected_swift_code: expected_swift_code(),
expected_c_header: EXPECTED_C_HEADER,
}
.test();
}
}

Anytime we add support for a new type we add a codegen test and an integration test.

In this case the new type that we are explicitly supporting (now that you're adding this test) is -> Result<TransStruct, TransStruct>


impl std::fmt::Debug for ffi::ResultTestTransparentStruct {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self)
Copy link
Owner

Choose a reason for hiding this comment

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

Let's avoid implying that these implementations are being used.

unreachable!("Debug impl was added to pass type checking")


impl std::fmt::Display for ffi::ResultTestTransparentStruct {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
Copy link
Owner

Choose a reason for hiding this comment

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

unreachable!("Display impl was added to pass type checking")

@chinedufn
Copy link
Owner

Looks like you added -> Result<(), TransStruct> and we already had a codegen test for that?

So, we're good? Anything else you're waiting for?

If not I can merge this once tests pass.

@sax
Copy link
Contributor Author

sax commented Jun 20, 2024

I'm adding one more codegen test that seems like it's missing for Result<(), TransStruct>, and then seems good.

@sax
Copy link
Contributor Author

sax commented Jun 20, 2024

Ok seems good to go! Thanks for the reviews. Also feels good to add a bit more test coverage to using transparent structs in results.

@chinedufn chinedufn merged commit 34ce1ff into chinedufn:master Jun 21, 2024
5 checks passed
@CGamesPlay
Copy link

Is it possible to make a new release with this change? It doesn't seem possible to work around from user code. Thank you!

@chinedufn
Copy link
Owner

chinedufn commented Aug 8, 2024

@CGamesPlay
Copy link

Thanks! Can confirm it fixes the warning for my transparent enum.

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.

ffi struct produces warnings on Rust 1.79.0 or +nightly
3 participants