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

regression in method dispatch #44224

Closed
nikomatsakis opened this issue Aug 31, 2017 · 25 comments
Closed

regression in method dispatch #44224

nikomatsakis opened this issue Aug 31, 2017 · 25 comments
Labels
C-bug Category: This is a bug. final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. regression-from-stable-to-nightly Performance or correctness regression from stable to nightly. T-lang Relevant to the language team, which will review and decide on the PR/issue.
Milestone

Comments

@nikomatsakis
Copy link
Contributor

According to @SimonSapin, #43880 likely broke Servo's build. Servo itself is fixed in servo/servo#18327, but I'm opening this issue to decide on whether this regression is a bug or expected behavior.

cc @arielb1

@nikomatsakis nikomatsakis added I-nominated regression-from-stable-to-nightly Performance or correctness regression from stable to nightly. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Aug 31, 2017
@SimonSapin
Copy link
Contributor

The code that broke is:

#[derive(JSTraceable)]
pub enum Filter {
    None,
    Native(fn (node: &Node) -> u16),
    JS(Rc<NodeFilter>)
}
/// A trait to allow tracing (only) DOM objects.
pub unsafe trait JSTraceable {
    /// Trace `self`.
    unsafe fn trace(&self, trc: *mut JSTracer);
}

unsafe impl<A, B> JSTraceable for fn(A) -> B {
    #[inline]
    unsafe fn trace(&self, _: *mut JSTracer) {
        // Do nothing
    }
}
#[proc_macro_derive(JSTraceable)]
pub fn expand_token_stream(input: proc_macro::TokenStream) -> proc_macro::TokenStream {

    // ...

    let style = synstructure::BindStyle::Ref.into();
    let match_body = synstructure::each_field(&mut type_, &style, |binding| {
        Some(quote! { #binding.trace(tracer); })
    });

    // ...

I think the generated code would look something like this:

unsafe impl JSTraceable for Filter {
    unsafe fn trace(&self, tracer: *mut JSTracer) {
        match *self {
            Filter::Native(ref field1) => {
                field1.trace(tracer);
            }
            // ...
        }
    }
}

The error message in rustc 1.21.0-nightly (7eeac1b 2017-08-30) was:

   Compiling script v0.0.1 (file:///home/simon/servo2/components/script)
error[E0599]: no method named `trace` found for type `&fn(&dom::node::Node) -> u16` in the current scope
   --> /home/simon/servo2/components/script/dom/treewalker.rs:464:10
    |
464 | #[derive(JSTraceable)]
    |          ^^^^^^^^^^^
    |
    = note: JSTraceable is a function, perhaps you wish to call it
    = help: items from traits can only be used if the trait is implemented and in scope
    = note: the following trait defines an item `trace`, perhaps you need to implement it:
            candidate #1: `dom::bindings::trace::JSTraceable`

error: aborting due to previous error

error: Could not compile `script`.

The fix was adding a new impl for fn(&A) -> B, in addition to the one for fn(A) -> B. Isn’t the former a subset of the types covered by the latter?

unsafe impl<'a, A, B> JSTraceable for fn(&A) -> B {
    #[inline]
    unsafe fn trace(&self, _: *mut JSTracer) {
        // Do nothing
    }
}

@shepmaster shepmaster added the C-bug Category: This is a bug. label Sep 1, 2017
@alexcrichton alexcrichton added this to the 1.22 milestone Sep 2, 2017
@nikomatsakis
Copy link
Contributor Author

nikomatsakis commented Sep 5, 2017

@SimonSapin thanks for the detailed notes.

I was talking to @arielb1 and I understand better the reason for the change. I am of two minds about it, to be honest. In short, under the older algorithm, we integrate "impl selection" directly into method dispatch. In particular, for each potentially applicable impl, we would instantiate the impl and then check whether the receiver type was a valid subtype of what the impl expected. In this case, the receiver type is for<'a> fn(&'a u8). This is a valid subtype of fn(A) where A = &'x u8, because 'a can be instantiated as 'x. Under the newer algorithm, we use trait selection instead. This creates a problem: an impl like impl<A> Foo for fn(A) doesn't implement Foo for for<'a> fn(&u8), because of the placement of the region binders.

This handling of region binders is a subtle point in Rust, and one that I actually consider a borderline bug. In particular, I'm in the process of various refactorings that aim to make the internal handling of higher-ranked things more consistent with all other regions, which offers a lot of simplicity across the board. But one side-effect is that we would no longer permit an impl of Foo for both fn(&u8) and fn(A) -- in other words, the workaround that @SimonSapin put in place would no longer work, which seems suboptimal. (In particular, I aim to have region erasure erase region bindings altogether.)

(It may be that these refactorings I would like to do can't be done the way I wanted to do them, though, since there could be other code relying on the existing behavior; it's hard to be sure without a working branch to test with (which I think is close)).

@SimonSapin
Copy link
Contributor

SimonSapin commented Sep 5, 2017

because of the placement of the region binders.

I don’t understand what that means. Is this a concept of the language that is relevant to its users, or only an internal detail of the compiler’s implementation?

But one side-effect is that we would no longer permit an impl of Foo for both fn(&u8) and fn(A)

Right, #43880 was a breaking change so effectively reverting it is another breaking change (unless it happens within the same release cycle).

The team needs to decide whether this breakage is acceptable per https://github.com/rust-lang/rfcs/blob/master/text/1122-language-semver.md.

@SimonSapin
Copy link
Contributor

And the larger policy question is: “it doesn’t affect any code that happens to be tested by crater/cargobomb” sufficient to justify any breaking change, even if it not a soundness fix?

@nikomatsakis
Copy link
Contributor Author

@SimonSapin

I don’t understand what that means. Is this a concept of the language that is relevant to its users, or only an internal detail of the compiler’s implementation?

Sorry, I geeked out a bit in the response there. The answer is somewhere in the middle. In short, there are corner cases of the language that interact with particulars in the implementation. Sometimes this gives rise to complicated or internally inconsistent definitions. The goal of #43880 was effectively to remove some "conceptual duplication" between how trait matching worked during method dispatch and how it works at other times.

The changes I was describing around subtyping and higher-ranked regions are another such case. They are independent except that they both interact on this particular example.

Right, #43880 was a breaking change so effectively reverting it is another breaking change (unless it happens within the same release cycle).

This does not necessarily -- that is, I don't know that @arielb1's new method dispatch code accepts anything in particular that the old code did not, but I could be wrong.

The team needs to decide whether this breakage is acceptable per https://github.com/rust-lang/rfcs/blob/master/text/1122-language-semver.md.

Indeed, we do. In my view, all of the changes under discussion fall roughly under the "Underspecified language semantics" heading. This is not to say we should necessarily make those changes -- just that they are the kinds of changes that arise we seek to clean up the compiler implementation and the definition of the trait system. In such cases, the "real-world effects" (i.e., how much code is affected) is definitely a consideration, as is the magnitude of the cruft.

@SimonSapin
Copy link
Contributor

In my view, all of the changes under discussion fall roughly under the "Underspecified language semantics" heading.

Alright, that's fair enough.

And as @aturon just said on IRC it's also the role of Nightly to catch issues like this that might have slipped through cargobomb, with time before they reach Stable.

@nikomatsakis nikomatsakis added the T-lang Relevant to the language team, which will review and decide on the PR/issue. label Sep 7, 2017
@nikomatsakis nikomatsakis removed the T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. label Sep 14, 2017
@nikomatsakis
Copy link
Contributor Author

@rfcbot fcp close

OK, having had time to think this over, I think the general feeling is that the change here was both permitted and a good change, although the regression is unfortunate. The intention is to clarify and improve method dispatch, and in particular to avoid having two "similar but different in subtle ways" implementations of how to match a trait against impls.

This does however mean that the behavior with respect to function types, consolidating on the behavior using during trait selection (which hence requires that the type of receiver be equal to the type in the trait impl, rather than being a subtype).

I'm going to move that we close this issue, therefore.

@rfcbot
Copy link

rfcbot commented Sep 14, 2017

Team member @nikomatsakis has proposed to close this. The next step is review by the rest of the tagged teams:

No concerns currently listed.

Once these reviewers reach consensus, this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added the proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. label Sep 14, 2017
@SimonSapin
Copy link
Contributor

I think the general feeling is that the change here was […] a good change

Do you mean that the language should not be changed later to make impl<A, B> SomeTrait for fn(A) -> B apply again to fn(&'a Foo) -> Bar?

@withoutboats
Copy link
Contributor

Could we crater this?

@SimonSapin
Copy link
Contributor

Cargbomb was used: #43880 (comment) but unfortunately it did not include Servo. (BTW could we fix that?)

@withoutboats
Copy link
Contributor

Thanks for the info!

@eddyb
Copy link
Member

eddyb commented Sep 15, 2017

cc @rust-lang/infra on #44224 (comment).

@SimonSapin
Copy link
Contributor

Let’s move the servo-in-cargobomb discussion to a separate thread: rust-lang/crater#133

@arielb1
Copy link
Contributor

arielb1 commented Sep 17, 2017

Changes in method matching against higher-ranked types

One edge-case in Rust's type-system is higher-ranked types (don't confuse with higher-kinded types, which aren't supported or planned to be supported). In plain words, these are types that contain a for<'lifetime> in them - for example, for<'a> fn(&'a Foo) -> Bar. In Rust, these are always function pointer or trait object types.

Part A - Incomplete impls

One problem with these types, is that they can't directly be decomposed into smaller types. for<'a> fn(&'a Foo) -> Bar is not equal to fn(T) -> Bar for any T (after all, what would that T be? for<'a> &'a Foo? That is not a type, and would give us fn(for<'a> &'a Foo) -> Bar, which has the for in the wrong place).

This means that traits can't be implemented fully generically for them, as they won't be matched by an impl<A, B> JSTraceable for fn(A) -> B. You have to write a more specific impl, like impl<A, B> JSTraceable for for<'a> fn(&'a A) -> B.

That was always the case, and probably will always be the case - you simply can't pick a value for A in that example.

NB: With higher-ranked types you could theoretically have something like impl<A<*>, B<*>> JSTraceable for for<'a> fn(A<'a>) -> B<'a>, but that opens it own can of worms.

Part B - Imperfect subtyping

The reason you could get away with only 1 impl is that there's subtyping between higher-ranked types and their concrete versions (actually, this is a kind of "subtyping" that even Haskell has, they just doesn't call it that way), which means that code like this compiles:

let my_fn : &fn(_) -> _ = &my_fn; // trigger subtyping
JSTraceable::trace(my_fn, tracer);

Now you would think subtyping would work automatically, without any annotations. In most situations, this is how it works. However, another annoyance with HRTs is that type-inference involving them is undecidable, which means compilers need to perform some hacks to make it work.

The hack rustc uses is that higher-ranked subtyping is "eager", like a coercion - if there are no type hints available at the moment subtyping would be done, it does not trigger. This means that in an example like JSTraceable::trace(&my_fn, tracer), the opportunity for subtyping would be missed and you'll get an error without the type hint.

If you add the type hint, everything works fine (yes I know this is hard to do with macros).

The Change

Before #43880, trait method selection used to provide type hints based on impls. So it would notice the impl<A, B> JSTraceable for fn(A) -> B impl, and try to perform subtyping to get it to match.

That type hint action was a pirate feature that complicated method dispatch and added a bunch of odd edge cases to the compiler, so it was removed. That means the opportunity for higher-ranked subtyping is missed, and you get a type error.

@SimonSapin
Copy link
Contributor

Thanks for the detailed explanation @arielb1. This all makes sense. At this point I’m only worried about what @nikomatsakis wrote earlier:

I'm in the process of various refactorings that aim to make the internal handling of higher-ranked things more consistent with all other regions, which offers a lot of simplicity across the board. But one side-effect is that we would no longer permit an impl of Foo for both fn(&u8) and fn(A) -- in other words, the workaround that @SimonSapin put in place would no longer work, which seems suboptimal. (In particular, I aim to have region erasure erase region bindings altogether.)

Why would impl<'a, A> Foo for fn(&'a A) and impl<A> Foo for fn(A) not be both permitted, if fn(&u8) never matches the latter?

@arielb1
Copy link
Contributor

arielb1 commented Sep 18, 2017

Why would impl<'a, A> Foo for fn(&'a A) and impl<A> Foo for fn(A) not be both permitted, if fn(&u8) never matches the latter?

We were talking about some theoretical concerns, but here's the practical thing: we might want for<'b> fn(&'α &'b u32) (for some "free" lifetime ) to be equal to fn(&'α &'α u32). If we want to allow that, we obviously can't allow there to be 2 separate impls.

Let's see why is that is sane:

  1. Obviously, for<'b> fn(&'α &'b u32) is a subtype of fn(&'α &'α u32) (just substitute 'b ← 'α).
  2. In the other direction: suppose we have a fn(&'α &'α u32), and we want to use it as an for<'b> fn(&'α &'b u32).
    A for<'b> fn(&'α &'b u32) is actually a for<'b> WF(&'α &'b u32) ⇒ fn(&'α &'b u32) - the caller has to prove that it is well-formed before calling it (this is just how higher-ranked functions work in Rust). However, that means that the assigned lifetime for 'b must be longer than , which means the argument is a valid &'α &'α u32. So calling the fn(&'α &'α u32) is sound.

Now, if we want, we can take account of that other subtyping as a part of our subtyping rules, and by antisymmetry of subtyping (which I think we'll like to maintain) add that equality to our type equality rules. However, I'm not sure we really want to implement it - it feels like a too confusing addition to an already-complicated thing - HRTs are already complicated enough on their own, and type equality is by itself complicated enough on its own, so doing a breaking change in preparation for a feature we don't actually want to implement feels silly. cc @nikomatsakis .

@arielb1
Copy link
Contributor

arielb1 commented Sep 18, 2017

I'll note that you can't do this sort of subtyping in today's trait dispatch, because it doesn't assume the trait's arguments are WF in order to do the subtyping.

trait Foo<'a> {
    fn foo<'b>(x: &'a &'b u32) {}
}

impl<'a> Foo<'a> for () {
    fn foo(x: &'a &'a u32) {} //~ ERROR
}

@cramertj
Copy link
Member

cramertj commented Sep 18, 2017

@rfcbot reviewed

(I don't have permission to check boxes in rust-lang/rust)

@eddyb
Copy link
Member

eddyb commented Sep 18, 2017

@cramertj Pretty sure that permission is literally "editing anyone else's comments".

  • yup

@cramertj
Copy link
Member

@eddyb Yes, but I have that permission in rust-lang/rfcs 😄

@aturon
Copy link
Member

aturon commented Oct 19, 2017

Checked for @nrc, who is away.

@rfcbot rfcbot added final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. and removed proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. labels Oct 19, 2017
@rfcbot
Copy link

rfcbot commented Oct 19, 2017

🔔 This is now entering its final comment period, as per the review above. 🔔

@rfcbot
Copy link

rfcbot commented Oct 29, 2017

The final comment period is now complete.

netbsd-srcmastr pushed a commit to NetBSD/pkgsrc that referenced this issue Nov 3, 2017
Changelog:
Version 1.21.0 (2017-10-12)
==========================

Language
--------
- [You can now use static references for literals.][43838]
  Example:
  ```rust
  fn main() {
      let x: &'static u32 = &0;
  }
  ```
- [Relaxed path syntax. Optional `::` before `<` is now allowed in all contexts.][43540]
  Example:
  ```rust
  my_macro!(Vec<i32>::new); // Always worked
  my_macro!(Vec::<i32>::new); // Now works
  ```

Compiler
--------
- [Upgraded jemalloc to 4.5.0][43911]
- [Enabled unwinding panics on Redox][43917]
- [Now runs LLVM in parallel during translation phase.][43506]
  This should reduce peak memory usage.

Libraries
---------
- [Generate builtin impls for `Clone` for all arrays and tuples that
  are `T: Clone`][43690]
- [`Stdin`, `Stdout`, and `Stderr` now implement `AsRawFd`.][43459]
- [`Rc` and `Arc` now implement `From<&[T]> where T: Clone`, `From<str>`,
  `From<String>`, `From<Box<T>> where T: ?Sized`, and `From<Vec<T>>`.][42565]

Stabilized APIs
---------------

[`std::mem::discriminant`]

Cargo
-----
- [You can now call `cargo install` with multiple package names][cargo/4216]
- [Cargo commands inside a virtual workspace will now implicitly
  pass `--all`][cargo/4335]
- [Added a `[patch]` section to `Cargo.toml` to handle
  prepublication dependencies][cargo/4123] [RFC 1969]
- [`include` & `exclude` fields in `Cargo.toml` now accept gitignore
  like patterns][cargo/4270]
- [Added the `--all-targets` option][cargo/4400]
- [Using required dependencies as a feature is now deprecated and emits
  a warning][cargo/4364]


Misc
----
- [Cargo docs are moving][43916]
  to [doc.rust-lang.org/cargo](https://doc.rust-lang.org/cargo)
- [The rustdoc book is now available][43863]
  at [doc.rust-lang.org/rustdoc](https://doc.rust-lang.org/rustdoc)
- [Added a preview of RLS has been made available through rustup][44204]
  Install with `rustup component add rls-preview`
- [`std::os` documentation for Unix, Linux, and Windows now appears on doc.rust-lang.org][43348]
  Previously only showed `std::os::unix`.

Compatibility Notes
-------------------
- [Changes in method matching against higher-ranked types][43880] This may cause
  breakage in subtyping corner cases. [A more in-depth explanation is available.][info/43880]
- [rustc's JSON error output's byte position start at top of file.][42973]
  Was previously relative to the rustc's internal `CodeMap` struct which
  required the unstable library `libsyntax` to correctly use.
- [`unused_results` lint no longer ignores booleans][43728]

[42565]: rust-lang/rust#42565
[42973]: rust-lang/rust#42973
[43348]: rust-lang/rust#43348
[43459]: rust-lang/rust#43459
[43506]: rust-lang/rust#43506
[43540]: rust-lang/rust#43540
[43690]: rust-lang/rust#43690
[43728]: rust-lang/rust#43728
[43838]: rust-lang/rust#43838
[43863]: rust-lang/rust#43863
[43880]: rust-lang/rust#43880
[43911]: rust-lang/rust#43911
[43916]: rust-lang/rust#43916
[43917]: rust-lang/rust#43917
[44204]: rust-lang/rust#44204
[cargo/4123]: rust-lang/cargo#4123
[cargo/4216]: rust-lang/cargo#4216
[cargo/4270]: rust-lang/cargo#4270
[cargo/4335]: rust-lang/cargo#4335
[cargo/4364]: rust-lang/cargo#4364
[cargo/4400]: rust-lang/cargo#4400
[RFC 1969]: rust-lang/rfcs#1969
[info/43880]: rust-lang/rust#44224 (comment)
[`std::mem::discriminant`]: https://doc.rust-lang.org/std/mem/fn.discriminant.html
@nikomatsakis
Copy link
Contributor Author

Closing as resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-bug Category: This is a bug. final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. regression-from-stable-to-nightly Performance or correctness regression from stable to nightly. T-lang Relevant to the language team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests