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

Spurious 'type is not a member' #15177

Closed
OlegYch opened this issue May 13, 2022 · 25 comments · Fixed by #17086
Closed

Spurious 'type is not a member' #15177

OlegYch opened this issue May 13, 2022 · 25 comments · Fixed by #17086

Comments

@OlegYch
Copy link
Contributor

OlegYch commented May 13, 2022

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 OlegYch added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels May 13, 2022
@prolativ prolativ added area:typer compat:scala2 and removed stat:needs triage Every issue needs to have an "area" and "itype" label labels May 13, 2022
@prolativ
Copy link
Contributor

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?

@OlegYch
Copy link
Contributor Author

OlegYch commented May 14, 2022

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

@OlegYch
Copy link
Contributor Author

OlegYch commented May 14, 2022

sometimes it would compile incrementally, but fail after clean

@prolativ
Copy link
Contributor

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

@OlegYch
Copy link
Contributor Author

OlegYch commented May 16, 2022

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

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

@prolativ
Copy link
Contributor

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?

@OlegYch
Copy link
Contributor Author

OlegYch commented May 16, 2022

@prolativ yep

@OlegYch
Copy link
Contributor Author

OlegYch commented Jun 10, 2022

any news on this?

@OlegYch
Copy link
Contributor Author

OlegYch commented Jun 29, 2022

a bit simpler repro:

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

@OlegYch
Copy link
Contributor Author

OlegYch commented Jul 26, 2022

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

@OlegYch
Copy link
Contributor Author

OlegYch commented Jul 26, 2022

a workaround is to delete type parameter...

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

@OlegYch
Copy link
Contributor Author

OlegYch commented Mar 2, 2023

still an issue on 3.2.2

@SethTisue
Copy link
Member

SethTisue commented Mar 8, 2023

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]

@OlegYch
Copy link
Contributor Author

OlegYch commented Mar 8, 2023

yes, it is pretty random

@dwijnand
Copy link
Member

dwijnand commented Mar 8, 2023

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.

@SethTisue
Copy link
Member

SethTisue commented Mar 9, 2023

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
Copy link
Member

SethTisue commented Mar 9, 2023

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

@SethTisue
Copy link
Member

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

@som-snytt
Copy link
Contributor

that check needs to be done more gently

or more kindly

@dwijnand
Copy link
Member

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
Copy link
Member

@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.

@SethTisue
Copy link
Member

SethTisue commented Mar 15, 2023

I've been thinking on whether even a partial solution could be considered sufficient. 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.

@smarter
Copy link
Member

smarter commented Mar 15, 2023

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 #16664, so maybe the completion logic can be tweaked?

@SethTisue SethTisue removed their assignment Aug 13, 2023
@SethTisue
Copy link
Member

SethTisue commented May 30, 2024

Dale has an idea where instead of eagerly traversing B’s entire constructor, we would only traverse its type parameters (if it has any, e.g. in the class B[C](id: A.Id) version of the reproducer). That would cause those type parameters to be added to the body of B as type members, and it seems plausible that's enough for now, and it would be fine for the rest of the constructor to get lazily handled later.

(This wouldn't help the class B extends ... variant, but it's plausible we'll conclude that the two variants are different scenarios requiring different fixes.)

@SethTisue
Copy link
Member

SethTisue commented Jun 13, 2024

Dale has an idea where instead of eagerly traversing B’s entire constructor, we would only traverse its type parameters

This is the approach taken in his PR (#17086).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants