scala3 icon indicating copy to clipboard operation
scala3 copied to clipboard

Spurious 'type is not a member'

Open OlegYch opened this issue 2 years ago • 25 comments

Compiler version

3.0.0 - 3.2.0-RC1-bin-20220511-7c446ce-NIGHTLY

Minimized code

trait DomainIdProvider[T] {
  type Id = List[T]
}
object Country extends DomainIdProvider[Country]
case class Country(
    id: Country.Id,
) 

Output

type Id is not a member of object Country

Explanation

This always compiles fine on 2.13 and sometimes on 3. I couldn't find the exact rule when it compiles and when it doesn't. In the example above it would compile fine if i move case class definition before object, but in other cases this doesn't help.

OlegYch avatar May 13 '22 09:05 OlegYch

but in other cases this doesn't help

@OlegYch not sure if I got this right. Do you mean there are some cases when changing the order of declaration of a class and its companion doesn't make the code compile? Could you give an example then?

prolativ avatar May 13 '22 15:05 prolativ

@prolativ yep that's what i mean i couldn't minimise it though

OlegYch avatar May 14 '22 09:05 OlegYch

sometimes it would compile incrementally, but fail after clean

OlegYch avatar May 14 '22 09:05 OlegYch

If you have any unminimized examples, that would be helpful too anyway

prolativ avatar May 16 '22 08:05 prolativ

case class Province(code: Province.Id, symbol: String)
object Province extends DomainIdProvider[Province]

for example this compiles incrementally, but doesn't after clean

OlegYch avatar May 16 '22 09:05 OlegYch

I haven't managed to reproduce the problem for this case ... Do you put

trait DomainIdProvider[T] {
  type Id = List[T]
}

in another file and then try to compile both files with a single scalac command?

prolativ avatar May 16 '22 11:05 prolativ

@prolativ yep

OlegYch avatar May 16 '22 15:05 OlegYch

any news on this?

OlegYch avatar Jun 10 '22 20:06 OlegYch

a bit simpler repro:

class X[T] {
  type Id = T
}
object A extends X[B]
class B(id: A.Id)

OlegYch avatar Jun 29 '22 17:06 OlegYch

still an issue on 3.2.1-RC1-bin-20220723-1b299b3-NIGHTLY

OlegYch avatar Jul 26 '22 13:07 OlegYch

a workaround is to delete type parameter...

class X {
  type Id
}
object A extends X {
  type Id = B
}
class B(id: A.Id)

OlegYch avatar Jul 26 '22 14:07 OlegYch

still an issue on 3.2.2

OlegYch avatar Mar 02 '23 17:03 OlegYch

Dale points out that A and B mutually refer to each other — more specifically, A.Id and B mutually refer to each other – and such circular structures are always a danger area for being dependent on order of compilation — either across multiple files, or the order of definitions within a single file

as in, this compiles! (just swapping the order of A and B):

class X[T] {
  type Id = T
}
class B(id: A.Id) 
object A extends X[B]

SethTisue avatar Mar 08 '23 18:03 SethTisue

yes, it is pretty random

OlegYch avatar Mar 08 '23 18:03 OlegYch

I ran into the same problem in #16924, because there's a test case in the repo that has a somewhat similar situation, where if one file is typed before the other, with my change you end up querying the children of a top level sealed trait before typing and discovering a nested second child... This issue seems like the easier version of the problem to study.

dwijnand avatar Mar 08 '23 18:03 dwijnand

an alternate form that also fails:

class X[T] { trait Id }
object A extends X[B]
class B(id: A.Id)

this seems slightly simpler to me — it shows that we need a type parameter (T), and we need an inherited member (Id), but the two needn't be connected to each other

it's unfortunate that the compiler feels it needs to process B's constructor so early. at the time A first mentions B, we need to enter B, but can we put off looking at B's constructor until later?

SethTisue avatar Mar 09 '23 16:03 SethTisue

Dale has found that the isSameKindAs check seems be the culprit — that check needs to be done more gently

SethTisue avatar Mar 09 '23 16:03 SethTisue

a user on Discord hit this today — their version looks like https://scastie.scala-lang.org/KmklIKFYTmKzmeEgwNQfuA

SethTisue avatar Mar 10 '23 01:03 SethTisue

that check needs to be done more gently

or more kindly

som-snytt avatar Mar 10 '23 06:03 som-snytt

So, we can short-circuit the kindness check for the Oleg's minimisation:

+    val selfParams = self.typeParams
+    val otherParams = other.typeParams
+    if selfParams.isEmpty && otherParams.isEmpty then true // defer calling hkResult to avoid completing types
+    else
       val selfResult  = self.hkResult
       val otherResult = other.hkResult

But then what about the same case, with only the change that the referenced type actually is higher-kinded?

// like tests/pos/i15177.scala but with B being higher kinded
class X[T[_]] {
  type Id
}
object A extends X[B]
class B[C](id: A.Id)

Well, before we even get to doing the kind checks for A we need to check/make X[B] legal, which it only is if we can eta-expand B such that it fits T[_], and that requires completing B.

So, then, why does completing B require completing B primary constructor? Well, if I'm understanding this right, I think that's required because constructor parameters can be a part of the parent type:

    /** Ensure constructor is completed so that any parameter accessors
     *  which have type trees deriving from its parameters can be
     *  completed in turn. Note that parent types access such parameter
     *  accessors, that's why the constructor needs to be completed before
     *  the parent types are elaborated.
     */
    def completeConstructor(denot: SymDenotation): Unit = {

So should we fix it for non-higher kinded types only? I think that would fix the FakeEnum case too.

dwijnand avatar Mar 11 '23 10:03 dwijnand

@smarter wdyt? Should we try and allow some of this? I have a patch that makes some attempts at progress for the applied type case (object A extends X[B[D]]) but it's incomplete. And perhaps there's some way to do it for eta-expanding too, maybe.

dwijnand avatar Mar 13 '23 10:03 dwijnand

I've been thinking on this. Naturally we can't in general promise to support absolutely everything that compiled in Scala 2. But we should default to trying to, within reason, depending on context. And here the context is a code pattern that looks simple enough and reasonable enough to me, plus it's been hit by multiple users, that I think we should try for backward compatibility for the known use cases, even if we don't have a fully general solution.

SethTisue avatar Mar 15 '23 17:03 SethTisue

I think that's required because constructor parameters can be a part of the parent type

They can be part of the parent tree (class Foo(x: Int) extends Bla(x)) but they're no longer allowed to be part of the parent type (class Foo(x: Int) extends Bla[x.type]) as of https://github.com/lampepfl/dotty/pull/16664, so maybe the completion logic can be tweaked?

smarter avatar Mar 15 '23 17:03 smarter