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

Local classes are uncheckable (type tests) #15134

Merged
merged 8 commits into from
Jul 6, 2022

Conversation

dwijnand
Copy link
Member

@dwijnand dwijnand commented May 6, 2022

No description provided.

@dwijnand
Copy link
Member Author

dwijnand commented May 9, 2022

I didn't look into the detail, but it looks like doing it so bluntly breaks:

 - dotty.communitybuild.CommunityBuildTestB.http4s
 - dotty.communitybuild.CommunityBuildTestB.scodec
 - dotty.communitybuild.CommunityBuildTestB.fs2

@dwijnand dwijnand closed this May 9, 2022
@dwijnand dwijnand deleted the local-classes-are-uncheckable branch May 9, 2022 10:53
@dwijnand dwijnand restored the local-classes-are-uncheckable branch May 10, 2022 11:18
@dwijnand dwijnand reopened this May 10, 2022
@dwijnand dwijnand force-pushed the local-classes-are-uncheckable branch 3 times, most recently from 8e1e2fc to d2a12f1 Compare May 10, 2022 13:07
@dwijnand dwijnand marked this pull request as ready for review May 11, 2022 12:41
@dwijnand dwijnand requested a review from odersky May 11, 2022 12:41
@dwijnand
Copy link
Member Author

@smarter maybe you're interested to review this, seeing as it's an old one you created.

@smarter
Copy link
Member

smarter commented May 11, 2022

No time to review this unfortunately, but traversing all types of all declarations of all base classes could be pretty expensive.

@dwijnand
Copy link
Member Author

dwijnand commented May 11, 2022

It's all types of all declarations of all base classes defined in the method, which is hopefully ok. And I'm all ears for whether there's a better way to do it.

Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

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

I also think we should keep searching for a more efficient way to fix this. But I don't have the time right now to do it. So it's probably best to revisit this later.

@odersky odersky removed their assignment May 26, 2022
@smarter
Copy link
Member

smarter commented May 28, 2022

Maybe we should consider all type tests to local classes as unchecked by default, the one case which is safe is when we go from one local class to another local class defined in the same scope:

def foo = {
  sealed trait A
  class B extends A
  val x: A = new B
  x match { case x: B => x }
}

Anything else could end up giving you an instance of a local class coming from a different call to the same method which semantically is a different class.

@smarter
Copy link
Member

smarter commented May 28, 2022

In other words the check would be something like "(x: A).isInstanceOf[B] emits a warning if B is a local class which is not statically reachable from the scope where A is defined"

@dwijnand
Copy link
Member Author

Anything else could end up giving you an instance of a local class coming from a different call to the same method which semantically is a different class.

That seems overly strict. A local class that doesn't depend on anything in the method surely isn't "unchecked" when used, such as:

class A
def foo = {
  class B extends A
  val x: A = new B
  x match {
    case x: B =>
    case _ =>
  }
}

But I did realise from your comment that I'm not considering singleton types based on any terms in the method, like any parameters.

Going back to performance, the check here sounds big, but it's "only" on the types of the declarations in the local classes. So if you have a local class with 30 methods, or 10 local classes with 3 methods, it will take the time that takes. But I think it's fairly limited, as local classes tend to be few and small, in my experience. In the case of fs2, https://github.com/dotty-staging/fs2/blob/main/core/shared/src/main/scala/fs2/Pull.scala#L824-L1260 I think I counted 8 classes with about 5 methods each, which seems excessive to me. I'm also not caching this information anywhere, admittingly, so I could look into that if desired.

@smarter
Copy link
Member

smarter commented May 29, 2022

Your example matches the exception to the unchecked warning I gave above.

@dwijnand
Copy link
Member Author

dwijnand commented May 30, 2022

Oh. Then how are you defining "statically reachable"? Because I'd assumed being defined in a def disqualified it.

@smarter
Copy link
Member

smarter commented May 30, 2022

I meant reachable through only static references from a specific scope (which might be the scope of a def)

@smarter
Copy link
Member

smarter commented May 30, 2022

Oh but actually I misread your example, i didn't see that A was defined outside the def and B inside. Then I assert that we should get an unchecked warning: in general you cannot know if the B you get was created in the current call to the def or in a different call and escaped, and in the latter case it's really a different class B that happens to have the same runtime representation

@dwijnand
Copy link
Member Author

Right. Do we have some verbiage to back up that claim? Because I think many users consider the fact that classes can be defined locally to be a proximity convenience rather than also implying method-call uniqueness in those class types. I find it particularly hard to defend that claim because it's impossible to build a true neg test for that match, using my class definitions.

@smarter
Copy link
Member

smarter commented May 30, 2022

It makes sense by analogy with inner classes (which store an outer pointer to allow for checked type tests)

@odersky
Copy link
Contributor

odersky commented Jun 12, 2022

Note: classSymbols is not the same as baseClasses: classSymbols of Foo | Bar is {Foo, Bar}.

@smarter
Copy link
Member

smarter commented Jun 12, 2022

classSymbols of Foo | Bar is {Foo, Bar}.

Is it? classSymbols calls parentSymbols which does https://github.com/lampepfl/dotty/blob/0260d758d89d01ab445c471aa159ca8367d596ba/compiler/src/dotty/tools/dotc/core/Types.scala#L539-L540
(not sure what the spec in question is)

@odersky
Copy link
Contributor

odersky commented Jun 12, 2022

Ah sorry, I meant Bar & Foo. For Bar | Foo, classSymbols is empty. But in any case it has nothing to do with baseClasses.

@dwijnand
Copy link
Member Author

Right, sorry, parentSymbols, not baseClasses.

Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

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

Otherwise LGTM

case _ => true
})

val res = recur(X.widen, replaceP(P))
val res = X.widenTermRefExpr.hasAnnotation(defn.UncheckedAnnot) || recur(X.widen, replaceP(P))
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this additional test needed?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is for whether the scrutinee was annotated @unchecked:

  def test8[T](x: T): T =
    class A(val elem: T)
    val p = prev
    (p: @unchecked) match
      case prev: A => prev.elem
      case _       => prev = new A(x); x

@odersky odersky assigned dwijnand and unassigned odersky Jul 5, 2022
@dwijnand dwijnand assigned odersky and unassigned dwijnand Jul 5, 2022
@dwijnand
Copy link
Member Author

dwijnand commented Jul 5, 2022

Let me know if you're happy with my answer (assuming, as I do, that CI passes).

@dwijnand dwijnand force-pushed the local-classes-are-uncheckable branch from fd61bf2 to 3ca5b70 Compare July 6, 2022 15:12
Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

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

Yes, looks all good to me now,

@odersky odersky merged commit 9d07d52 into scala:main Jul 6, 2022
@dwijnand dwijnand deleted the local-classes-are-uncheckable branch July 6, 2022 17:39
@Kordyjan Kordyjan added this to the 3.2.1 milestone Aug 1, 2023
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.

Pattern matching on a local class is unsound, should emit an unchecked warning
4 participants