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

DFE fails to resolve methods inherited from mixin #29758

Closed
aam opened this issue May 31, 2017 · 55 comments
Closed

DFE fails to resolve methods inherited from mixin #29758

aam opened this issue May 31, 2017 · 55 comments
Assignees
Labels
area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language).

Comments

@aam
Copy link
Contributor

aam commented May 31, 2017

In Flutter code super.attach(owner); in RenderObjectWithChildMixin that is used as a mixin for RenderView should be resolved to RenderObject#attach but it gets unresolved:

file:///flutter/flutter/packages/flutter/lib/src/rendering/object.dart:2741:5: Warning: Method not found: 'super.attach'.
    super.attach(owner);
    ^
@aam
Copy link
Contributor Author

aam commented May 31, 2017

Here is a snippet that demonstrates the problem:

class Base {
  void attach(String s) {
    print("Base attach $s");
  }
}

class ViewBase {
  void attach(String s) {
    print("Viewbase attach $s");
  }
}

abstract class Mixin implements Base {
  void attach(String s) {
    super.attach(s);
    print("Mixin attach $s");
  }
}

class View extends ViewBase with Mixin {
}

void main() {
  new View().attach("me");
}

Dart VM is happy to parse and run it:

$ ~/dart/dart-sdk/sdk/xcodebuild/DebugX64/dart mixin.dart 
Viewbase attach me
Mixin attach me
$ 

Both dartanalyzer and dart2js complain about Mixin referencing super

$ ~/dart/dart-sdk/sdk/xcodebuild/DebugX64/dart-sdk/bin/dartanalyzer mixin.dart 
Analyzing mixin.dart...
  error • The class 'Mixin' can't be used as a mixin because it references 'super' at mixin.dart:20:34 • mixin_references_super
1 error found.
$ ~/dart/dart-sdk/sdk/xcodebuild/DebugX64/dart-sdk/bin/dart2js mixin.dart 
mixin.dart:15:5:
Warning: Can't resolve 'attach' in a superclass of 'Mixin'.
    super.attach(s);
    ^^^^^^^^^^^^^^^
mixin.dart:20:34:
Error: Cannot use class 'Mixin' as a mixin because it uses 'super'.
class View extends ViewBase with Mixin {
                                 ^^^^^
mixin.dart:15:5:
Info: Use of 'super' in class used as mixin.
    super.attach(s);
    ^^^^^^^^^^^^^^^
Error: Compilation failed.
$ 

@a-siva
Copy link
Contributor

a-siva commented May 31, 2017

@floitschG

@aam
Copy link
Contributor Author

aam commented May 31, 2017

dart2js and dartanalyzer errors perhaps should be disregarded as they seem to be out of date with DEP https://github.com/gbracha/lessRestrictedMixins.

Per that DEP, DFE seems to be actually correct with it's diagnostic that super.attach() method is not found.

@floitschG
Copy link
Contributor

floitschG commented May 31, 2017

I believe that the proposal requires an "extends" clause and not just "implements".

dart2js hasn't implemented this DEP.
I'm not sure about the analyzer, but I'm guessing it doesn't support it either.

Edit: since Peter assigned this issue to 'area-fasta', I'm guessing this might be related to the transition of the analyzer to the new frontend.

@sigmundch
Copy link
Member

correct that dart2js doesn't implement it, analyzer has it under the --supermixin flag.

@aam
Copy link
Contributor Author

aam commented May 31, 2017

@Hixie

@aam
Copy link
Contributor Author

aam commented Jun 1, 2017

Ian provided good feedback that using 'extends' on mixin declaration leaves possibility to extend mixin directly outside of mixin application. On the other hand, if you use 'implements', then attempt to extend mixin will result in analyze-time error. See http://shortn/_5rg2Fqu4Qi for the example.

@peter-ahe-google
Copy link
Contributor

From an implementation perspective, it would be extremely helpful if the work around of adding the superclass as specified could be used for now until the the language team has had a chance to hear feedback.

@aam
Copy link
Contributor Author

aam commented Jun 1, 2017

Sure, Peter, I agree!

@lrhn
Copy link
Member

lrhn commented Jun 1, 2017

The DEP has been implemented, so it's superseded by the ECMA language specification (4th Edition, Dec 2015), which contains the "new" mixin/super specification.

It's correct what Florian says, to have a super.m() invocation, your superclass'es interface must have an m() method.
When you apply a mixin derived from a class, A, with superclass B, the application must have a class that implements B. This (almost) ensures that the super.m() call has something to find (but it is possible to have an abstract superclass that doesn't have a concrete m method).

So

class B { int foo() => 37; }
class M extends B { int bar() => super.foo(); }
class A implements B { int foo() => 42; }
class N = A with M;

This works, A with M is allowed because A implements B, the superclass of M.

For the example above, if ViewBase implemented Base and Mixin extended Base, then it should be valid.

@peter-ahe-google
Copy link
Contributor

@lrhn in your example, M has no superclass that provides a foo method.

@lrhn
Copy link
Member

lrhn commented Jun 1, 2017

Thanks, fixed.

@peter-ahe-google
Copy link
Contributor

Phew :-)

@aam
Copy link
Contributor Author

aam commented Jun 1, 2017

@lrhn , thanks! Can you please weigh in on options(if any?) on how to make mixin classes final(not extendable)?

@peter-ahe-google
Copy link
Contributor

If you want to prevent people from subclassing any class, just add this:

  factory MyClass() => null;

This also works for mixins.

@aam
Copy link
Contributor Author

aam commented Jun 1, 2017

Ah, sorry, I meant prevent subclassing with analysis-time(rather than runtime) error(like http://shortn/_5rg2Fqu4Qi describes).

@peter-ahe-google
Copy link
Contributor

This will also be a warning at analysis time.

@aam
Copy link
Contributor Author

aam commented Jun 1, 2017

Currently there seem to be no warnings produced by dartanalyzer. What the warning would be about?

$ cat mixin.dart
class B { int foo() => 37; }
class M extends B { int bar() => super.foo(); }
class A implements B { int foo() => 42; }
class N = A with M;
class N1 extends M {
  factory N1() => null;
}

main() {
  print (new N().bar());
  print (new N1().bar());
}
$ ~/dart-sdk1/sdk/out/DebugX64/dart-sdk/bin/dartanalyzer --supermixin mixin.dart
Analyzing mixin.dart...
No issues found!
$

@peter-ahe-google
Copy link
Contributor

This is what you want:

class M extends B {
  factory M() => null; // Prevent extending M.
  int bar() => super.foo();
}

@aam
Copy link
Contributor Author

aam commented Jun 1, 2017

Ah, got it, Thanks Peter!

@Hixie
Copy link
Contributor

Hixie commented Jun 1, 2017

factory Mixin._() => null; gives you a reasonable-ish analyzer message ("The superclass 'Mixin' doesn't have a zero argument constructor") and doesn't put anything into the dartdocs, so that's a satisfactory workaround for the places where Flutter is currently using "implements" to prevent extending a mixin.

@Hixie
Copy link
Contributor

Hixie commented Jun 1, 2017

@aam do you have a list of the places where Flutter is affected by this change? I'm happy to apply the fix.

@peter-ahe-google
Copy link
Contributor

Thank you, @Hixie

@Hixie
Copy link
Contributor

Hixie commented Jun 1, 2017

So looking at this more closely (@aam sent me the list of places to change), there is another problem with this. How do you specify that a mixin expects to only be used in a situation that also mixes in two other (unrelated) classes?

WidgetsBinding currently uses implements to specify that it needs to be used in a situation that mixes in both GestureBinding and RendererBinding.

Here's a reduced version of this problem, in the syntax that works today:

class RealSuper {
  RealSuper(this.quux);
  final int quux;
}

abstract class Mixin1 extends RealSuper {
  factory Mixin1._() => null;
  void foo() {
    print("Mixin1.foo $quux");
  }
}

abstract class Mixin2 extends RealSuper {
  factory Mixin2._() => null;
  void bar() {
    print("Mixin2.bar $quux");
  }
}

abstract class Mixin3 extends RealSuper implements Mixin1, Mixin2 {
  factory Mixin3._() => null;
  void baz() {
    print("Mixin3.baz $quux");
  }
  @override
  void foo() {
    print("Mixin3.foo $quux");
    super.foo();
  }
  @override
  void bar() {
    print("Mixin3.bar $quux");
    super.bar();
  }
}

class C extends RealSuper with Mixin1, Mixin2, Mixin3 {
  C() : super(1);
}

void main() {
  new C()
   ..foo()
   ..bar()
   ..baz();
}

How do I do this if we can't use "implements"?

@peter-ahe-google
Copy link
Contributor

@Hixie, I'm not sure I understand your question, but if I add these two methods to RealSuper, your example works:

  void foo() {}
  void bar() {}

Did you mean to make C extend RealSuper?

@Hixie
Copy link
Contributor

Hixie commented Jun 1, 2017

RealSuper doesn't know about foo and bar. They are introduced by Mixin1 and Mixin2 respectively.
I don't really mind too much what the code looks like so long as it is readable and gets the effect that we get today. The code snippet above is as I intended, it's what works today if you want to do this.

Abstract classes Mixin1 and Mixin2 introduce new features that can be used by concrete classes that implement the RealSuper interface. Mixin3 is intended for use by such concrete classes that mix in both Mixin1 and Mixin2. C is a class that mixes all three in.

@peter-ahe-google
Copy link
Contributor

Is this what you're looking for:

abstract class Mixin3 extends RealSuper with Mixin1, Mixin2 {
...

@Hixie
Copy link
Contributor

Hixie commented Jun 1, 2017

Aha, yup, that seems to work. Let me try to deploy that. Thanks!

@Hixie
Copy link
Contributor

Hixie commented Jun 2, 2017

class RealSuper {
  RealSuper(this.quux);
  final int quux;
}

abstract class Interface {
  factory Interface._() => null;
  void foo();
}

abstract class Mixin1 extends RealSuper with Interface {
  factory Mixin1._() => null;
  @override
  void foo() {
    print("Mixin1.foo $quux");
  }
}

// This mixin wants to be mixed into a class that inherits from a class that                                                                                                                              
// extends RealSuper and implements Interface. It doesn't know about Mixin1,                                                                                                                              
// which allows classes to easily satisfy those conditions.                                                                                                                                               
abstract class Mixin2 extends RealSuper implements Interface {
  factory Mixin2._() => null;
  @override
  void foo() {
    super.foo();
    print("Mixin2.foo $quux");
  }
}

class C extends RealSuper with Mixin1, Mixin2 {
  C() : super(1);
}

void main() {
  new C()
   ..foo();
}

@lrhn
Copy link
Member

lrhn commented Jun 3, 2017

Here you have abstract class Mixin1 extends RealSuper with Interface ...
That means that the superclass of Mixin1 is an anonymous class that applies the mixin derived from Interface to the class RealSuper.

When you later try to apply Mixin1 in RealSuper with Mixin1, Mixin2, you should get a warning because RealSuper doesn't implement the anonymous class from above.

The example is equivalent to the declarations:

class Anon = RealSuper with Interface;
abstract class Mixin1 extends Anon { ... }
class C extends RealSuper with Mixin1, Mixin2 { ... }  // Warning, RealSuper doesn't implement Anon.

(If you meant to write implements Interface in the declaration of Mixin1, there wouldn't be a warning about that).

@aam
Copy link
Contributor Author

aam commented Jun 3, 2017

@lrhn , currently the only warning DFE produces is at line 26 in method foo in Mixin2:

$ pkg/front_end/tool/fasta compile --platform=xcodebuild/DebugX64/patched_sdk/platform.dill --packages .packages ~/mixin2.dart 
file:///Users/aam/mixin2.dart:26:5: Warning: Method not found: 'super.foo'.
    super.foo();
    ^

because super.foo() is abstract method in RealSuper with Mixin1.

@Hixie
Copy link
Contributor

Hixie commented Jun 4, 2017

The only warnings I got from the last code snippet I pasted were that I hadn't documented the public APIs. It works great. :-)

@lrhn
Copy link
Member

lrhn commented Jun 4, 2017

If you're just running it in the VM, then you won't get any warnings (since the VM just don't do static warnings), but if the analyzer doesn't warn, then it's a bug.

A mixin application of the form S with M; defines a class C with superclass S.
A mixin application of the form S with M<sub>1</sub>, . . . , M<sub>k</sub>; defines a class C whose superclass is the application of the mixin composition (12.2) Mk−1 ∗ . . . ∗ M1/ to S.

and

Let MA be a mixin derived from a class M with direct superclass Sstatic.
Let A be an application of MA. It is a static warning if the superclass of A is not a subtype of *Sstatic.

together means that your program should have a warning.

Looking at Mixin2, there is another problem there, because it's calling super.foo(), and the superclass doesn't have, or declare, a foo. The superclass is RealSuper, and it has no foo declaration at all. The Mixin2 class also implements Interface itself, but its superclass doesn't.

A super method invocation i has the form
    super.m(a1, . . . , an, xn+1 : an+1, . . . , xn+k : an+k)
...
Let Sstatic be the superclass of the immediately enclosing class. It is a static type warning if Sstatic does not have an accessible (6.2) instance member named m unless Sstatic or a superinterface of Sstatic is annotated with an annotation denoting a constant identical to the constant @proxy defined in dart:core.

@Hixie
Copy link
Contributor

Hixie commented Jun 4, 2017

As implemented, this works, and is very useful. If the spec is less useful we should fix the spec.

@kasperl
Copy link

kasperl commented Jun 7, 2017

@Hixie and @aam: Am I right in assuming that you're still blocked here in terms of getting the new frontend to work with the Flutter code?

@aam
Copy link
Contributor Author

aam commented Jun 7, 2017

That's right, @kasperl ,
https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/rendering/binding.dart#L296 refers to super.hitTest(), which frontend doesn't resolve because it's an abstract method.

@Hixie
Copy link
Contributor

Hixie commented Jun 7, 2017

Yeah, we're looking for a way to do what is shown in the last snippet above: #29758 (comment)

@kasperl
Copy link

kasperl commented Jun 12, 2017

@peter-ahe-google: This issue is currently assigned to you, Peter. Are you planning any changes here or did this issue just happen to get stuck? @aam: Do you know what the current state of affairs is?

@aam
Copy link
Contributor Author

aam commented Jun 12, 2017

@kasperl , @lrhn is driving resolution as far as I understand, but I don't have any updates on this.

@peter-ahe-google
Copy link
Contributor

Any class that contains a method with a super call must extend a class that provides the method.

One way to accomplish that is to make sure that Interface.foo isn't an abstract method and have Mixin2 mixin Interface:

class RealSuper {
  RealSuper(this.quux);
  final int quux;
}

abstract class Interface {
  factory Interface._() => null;
  void foo() {}
}

abstract class Mixin1 extends RealSuper implements Interface {
  factory Mixin1._() => null;
  @override
  void foo() {
    print("Mixin1.foo $quux");
  }
}

abstract class Mixin2 extends RealSuper with Interface {
  factory Mixin2._() => null;
  @override
  void foo() {
    super.foo();
    print("Mixin2.foo $quux");
  }
}

class C extends RealSuper with Mixin1, Mixin2 {
  C() : super(1);
}

void main() {
  new C()..foo();
}

Using this approach, Mixin2 can be analyzed as a standalone class whose super calls are all fully resolved.

The guide line is: if you want to use super in a mixin, the mixin must be defined as a subclass of another class (or mixin application) that provides a non-abstract version of that method. This is no different from any other class that uses super.

I'm not sure why you define Mixin1 and Mixin2 as subclasses of RealSuper, this would be just as fine:

class RealSuper {
  RealSuper(this.quux);
  final int quux;
}

abstract class Interface {
  factory Interface._() => null;
  void foo() {}
}

abstract class Mixin1 implements RealSuper, Interface {
  factory Mixin1._() => null;
  @override
  void foo() {
    print("Mixin1.foo $quux");
  }
}

abstract class Mixin2 extends Interface implements RealSuper {
  factory Mixin2._() => null;
  @override
  void foo() {
    super.foo();
    print("Mixin2.foo $quux");
  }
}

class C extends RealSuper with Mixin1, Mixin2 {
  C() : super(1);
}

void main() {
  new C()..foo();
}

@Hixie
Copy link
Contributor

Hixie commented Jun 12, 2017

One way to accomplish that is to make sure that Interface.foo isn't an abstract method

That's not a solution, because Interface is out of your control (e.g. it comes from another package).

@Hixie
Copy link
Contributor

Hixie commented Jun 12, 2017

(In our specific case, Interface is actually an abstract class that other people extend.)

@Hixie
Copy link
Contributor

Hixie commented Jun 12, 2017

This case isn't academic, it's something that's deployed today in Flutter's framework, which works fine with today's VM and analyzer.

@peter-ahe-google
Copy link
Contributor

An alternative to adding the concrete implementation to Interface, you can add it to a private class:

class RealSuper {
  RealSuper(this.quux);
  final int quux;
}

abstract class Interface {
  factory Interface._() => null;
  void foo();
}

class _Interface implements Interface {
  void foo() {}
}

abstract class Mixin1 implements RealSuper, Interface {
  factory Mixin1._() => null;
  @override
  void foo() {
    print("Mixin1.foo $quux");
  }
}

abstract class Mixin2 extends _Interface implements RealSuper {
  factory Mixin2._() => null;
  @override
  void foo() {
    super.foo();
    print("Mixin2.foo $quux");
  }
}

class C extends RealSuper with Mixin1, Mixin2 {
  C() : super(1);
}

void main() {
  new C()..foo();
}

@peter-ahe-google
Copy link
Contributor

How should we handle this program:

class Interface {
  foo();
}

abstract class A implements Interface {
  foo() {
    super.foo();
  }
}

class B extends A {}

main() {
  new B().foo();
}

@kasperl kasperl added area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). and removed front-end-fasta labels Jun 12, 2017
@kasperl kasperl assigned lrhn and unassigned peter-ahe-google Jun 12, 2017
@kasperl
Copy link

kasperl commented Jun 12, 2017

I think the preferred way forward is to think about this as a language issue that probably needs both a short term and a longer term solution. @floitschG @lrhn: Are you okay with taking this issue over?

@kasperl kasperl reopened this Jun 12, 2017
@lrhn
Copy link
Member

lrhn commented Jun 13, 2017

Just to be clear: What is "this issue"? There has been a lot of discussion about various short-comings of the current mixin specification.
The original request was that something should work which would work if using extends instead of implements and the being-extensible can be fixed with hidden constructors.

That is:

class Base {
  void attach(String s) {
    print("Base attach $s");
  }
}

class ViewBase {
  void attach(String s) {
    print("Viewbase attach $s");
  }
}

abstract class Mixin extends Base {
  factory Mixin._() => throw "nope";
  void attach(String s) {
    super.attach(s);
    print("Mixin attach $s");
  }
}

class View extends ViewBase with Mixin {
}

void main() {
  new View().attach("me");
}

is a solution to the original issue.

There are two other issues mentioned:

  • Not warning when a mixin application causes a super-invocation to not have a target (statically detectable, but no warning given, just a runtime error).
  • Not being able to express multiple super-constraints (like having to be mixed onto both Mixin1 and Mixin2).

For the former, we can add a warning, something like:

It's a static warning if any member introduced by the mixin application contains a super-invocation (... list of super-invocation forms ...) and the superclass does not have a non-abstract member of that name.

The analyzer can give such a hint today if it wants to, we don't need to specify it now, but I'm sure it will become an official warning (strong-mode error) when we do address mixins properly.

For the "cannot have multiple super-constraints", I'm not sure there is a short-term solution, unless doing the proper redesign can be done in the short term.
The current specification is pretty clear and pretty broken in practice. I don't see any simple tweaks that can salvage it. As long as we declare classes and mixins with the same syntax, and their requirements are pretty much opposite of each other (you never want a class to call a super.method that doesn't exist, you almost always want a mixin to be allowed to do it). Also, we only have the one "extends" to work with when declaring the super-capabilities of the mixin (and that's what it is, it's what allows super-calls in the mixin methods, and it has to work for classes too as long as classes and mixins are the same).

@Hixie
Copy link
Contributor

Hixie commented Jun 13, 2017

How should we handle this program:

class Interface {
  foo();
}

abstract class A implements Interface {
  foo() {
    super.foo();
  }
}

class B extends A {}

main() {
  new B().foo();
}

I would imagine it would be handled the same as today.

Unhandled exception:
NoSuchMethodError: Super class of class 'B' has no instance method 'foo' with matching arguments.
Receiver: Instance of 'B'
Tried calling: super.foo()
Found: super.foo()
#0      Object._noSuchMethod (dart:core-patch/object_patch.dart:43)
#1      Object.noSuchMethod (dart:core-patch/object_patch.dart:47)
#2      A.foo (file:///home/ianh/dev/scratch/test.dart:7:16)
#3      main (file:///home/ianh/dev/scratch/test.dart:14:11)
#4      _startIsolate.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:265)
#5      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:151)
(exited)

You could also catch this at compile time, when B is defined, or when B is instantiated. My preference would be for catching it at compile time when B is defined.

@kasperl
Copy link

kasperl commented Jun 15, 2017

@Hixie, @aam, @lrhn: For what it's worth, I have a patch to the new frontend that allows the code in comment #29758 (comment) to run without issues. This could be a way to make progress and ease the transition to the new frontend -- even if the code is not conforming to the current specification.

The patch disables the warning on super.foo() if the class that contains it (Mixin2) implements an interface that has a foo member, but may fail at runtime with a noSuchMethod error if the mixin application containing the super.foo() call doesn't have a superclass with a concrete implementation.

@a-siva
Copy link
Contributor

a-siva commented Jan 29, 2018

This issue is being tracked in two other issues #31994 (short term fix to add a flag to suppress warnings/errors) and #31542 (longer term plan which involves language changes and a migration of flutter code). Closing this issue as we will use the other two to track this.

@a-siva a-siva closed this as completed Jan 29, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language).
Projects
None yet
Development

No branches or pull requests

8 participants