8000 SI-7046 partial fix to knownDirectSubclasses for reflection users and macro authors by milessabin · Pull Request #5284 · scala/scala · GitHub
[go: up one dir, main page]

Skip to content

SI-7046 partial fix to knownDirectSubclasses for reflection users and macro authors #5284

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

Merged
merged 1 commit into from
Dec 1, 2016

Conversation

milessabin
Copy link
Contributor
@milessabin milessabin commented Jul 15, 2016

This is a 95% fix of SI-7046 for comment and feedback.

This appears to do the right thing in the most typical scenarios in which knownDirectSubclasses would be used. The missing 5% is that subclasses defined in local scopes might not be seen by knownDirectSubclasses (see Local and Riddle in the test below). In mitigation, though, it is almost certain that a local subclass would represent an error in any scenario where knownDirectSubclasses might be used.

Errors for such situations are reported by recording (via a symbol attachment) that knownDirectSubclasses has been called and reporting an error if any additional children are added subsequently.

Despite these limitations and caveats, I believe that this represents a huge improvement over the status quo, and would eliminate 100% of the failures that I've seen in practice with people using shapeless for type class derivation.

Aside from that, all (par)tests pass.

@@ -1175,6 +1172,9 @@ trait ContextErrors {
def MissingParameterOrValTypeError(vparam: Tree) =
issueNormalTypeError(vparam, "missing parameter type")

def ParentSealedInheritanceError(parent: Tree, psym: Symbol) =
NormalTypeError(parent, "illegal inheritance from sealed " + psym )

Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need to reorder this error message?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's moved from ContextErrors to NamerContextErrors so that it's accessible in Namers.

@xeno-by
Copy link
Contributor
xeno-by commented Jul 15, 2016

I think that it may still be possible to address an important case of subclasses nested in a top-level object. Do you think you'll need an involved context for that?

@milessabin
Copy link
Contributor Author

@xeno-by the scenario you described is handled ... see the test.

@milessabin
Copy link
Contributor Author

Specifically Wibble and Wobble nested in object Foo.

@dwijnand
Copy link
Member

Maybe I'm just fixating too much on the name, but it's knownDirectSubclasses, not allDirectSubclasses - ie. known at the level in which you ask them, anything declared in some local scope isn't known.

So I would expect that if you called Test.subs in those local scopes it would see those subclass too. Perhaps it would be good to add those cases to the test.

@milessabin
Copy link
Contributor Author
milessabin commented Jul 15, 2016

@dwijnand yes, that's true, but I would prefer to see an error reported in that case ... it would be reintroducing a compilation order dependency which this PR is intended to eliminate.

@Blaisorblade
Copy link
Contributor

IIUC, the biggest potential danger is the added call to the new method forceDirectSuperclasses in knowDirectSubclasses, because it forces more types than before and that's a well-known source of bugs. The danger is a forcing too much too early; to me, earlier discussions on SI-7046 assumed (implicitly) this couldn't be done. And while tests pass, that might of course be insufficient.

I'd guess the patch should spell out (in code) an informal argument why that's safe, to allow inspecting it for corner cases and writing appropriate testcases.

In particular, this should be fine if classes are typechecked in inheritance order, but is that really the case in Scalac? Can we loop Scalac with this patch by calling knowDirectSubclasses on an inheritance cycle?

@milessabin
Copy link
Contributor Author

@Blaisorblade the use of maybeInitialize means that cycles are harmless. The Unrelated class explicitly tests the case where there is a cycle (via the reference to the macro-defined val which is triggering the force) ... this example was suggested by @retronym.

@xeno-by
Copy link
Contributor
xeno-by commented Jul 15, 2016

@milessabin Very interesting! Why?

@milessabin
Copy link
Contributor Author

@xeno-by we only care about initialising things sufficiently to ensure that children add themselves to their sealed parents. These types won't participate in a cycle (if they did it would be an error which we would expect to be reported), so if we initialise as far as we can we'll get the parent-child relationships that we need. If there are no objectionable cycles we'll get the correct (modulo local classes) result; if there are objectionable cycles they will be an error and be reported as such.

@notxcain
Copy link

Any chance of getting this in 2.12.0?

@@ -1675,6 +1696,9 @@ trait Namers extends MethodSynthesis {

abstract class TypeCompleter extends LazyType {
val tree: Tree
override def forceDirectSuperclasses: Unit = {
tree.foreach(t => Option(t.symbol).map(_.maybeInitialize))
Copy link
Member
@retronym retronym Jul 20, 2016

Choose a reason for hiding this comment

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

I believe this should be restricted to DefTrees:

tree.foreach {
  case dt: DefTree if dt.symbol != null => dt.symbol.maybeInitialize
  case _ =>
}

Copy link
Member

Choose a reason for hiding this comment

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

I think that maybeInitialize is a weak link in this chain. It is defined as:

    def maybeInitialize = {
      try   { initialize ; true }
      catch { case _: CyclicReference => debuglog("Hit cycle in maybeInitialize of $this") ; false }
    }

The problem is that this doesn't undo any side effects performed by the type completer before it ran into the cycle. The type completer will be run again, and we might observe problems if those side effects aren't idempotent.

maybeInitialize is currently only used in -Ybreak-cycles, and was described as "sketchy" when it landed: 432f936

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed on the DefTree restriction.

On the sketchiness of maybeInitialize ... yes, side-effects won't be undone. However any side effects that are triggered before a cycle is observed should be stable, ie. we're merely moving them to an earlier point rather than changing the outcome.

Also, this path will only be followed if knownDirectSubclasses is called, and the result can't be any sketchier than the status quo.

Could we run a community build with this change and see how it plays out?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@retronym any thoughts on the idea of using a flag to record that knownDirectSubclasses has been observed so that we can error if any additional subclasses add themselves later?

@milessabin
Copy link
Contributor Author

As an additional data point wrt maybeInitialize I can confirm that shapeless builds and tests perfectly with this patch applied. It has several uses of knownDirectSubclasses and these are exercised heavily in its tests and examples.

@retronym
Copy link
Member
retronym commented Jul 21, 2016

Here's a very contrived example to that shows a leakage in maybeInitialize.

package p1

import scala.reflect.macros.blackbox._
import language.experimental._

object Macro {
  def impl(c: Context): c.Tree = {
    import c.universe._
    println(rootMirror.staticClass("p1.Base").knownDirectSubclasses)
    q"()"
  }
  def p1_Base_knownDirectSubclasses: Unit = macro impl
}
package p1

sealed trait Base

object Test {
  val x = {
    Macro.p1_Base_knownDirectSubclasses
    ""
  } 
}

case class B(val b: Test.x.type)

This issues a cyclic error under this PR. I think this is because the CyclicError is being caught in typedInternal:

      try runTyper() catch {
        case ex: TypeError =>
          tree.clearType()
          // The only problematic case are (recoverable) cyclic reference errors which can pop up almost anywhere.
          typingStack.printTyping(tree, "caught %s: while typing %s".format(ex, tree)) //DEBUG
          reportTypeError(context, tree.pos, ex)
          setError(tree)
        case ex: Exception =>
          // @M causes cyclic reference error
          devWarning(s"exception when typing $tree, pt=$ptPlugins")
          if (context != null && context.unit.exists && tree != null)
            logError("AT: " + tree.pos, ex)
          throw ex
      }

rather than bubbling up to the catch-and-discard in maybeInitialize.

This isn't quite what I had in mind above, but it does qualify as a side-effect (issuing the type error).

< 8000 p dir="auto">The example itself needs a bit more minimization before its worthy of detailed discussion. But it shows basis for my unease about maybeInitialize is its current state.

I notice that you've put the forcing in directKnownSubclasses in the public reflection API which is good in the sense that this won't be triggered when the typer calls children in CheckabilityChecker; we know that the problems will only affect files that use shapeless-like macros.

I'm going to keep thinking about this one.

@milessabin
Copy link
Contributor Author

@retronym would be keen to get your take on the CyclicReference propagation fix in the most recent commit.

@milessabin
Copy link
Contributor Author
milessabin commented Jul 21, 2016

Failures appear to be transient ...

@milessabin
Copy link
Contributor Author

/rebuild

@@ -265,6 +265,9 @@ trait Contexts { self: Analyzer =>
def inSecondTry_=(value: Boolean) = this(SecondTry) = value
def inReturnExpr = this(ReturnExpr)
def inTypeConstructorAllowed = this(TypeConstructorAllowed)
def propagateCyclicReferences_=(value: Boolean)
= this(PropagateCyclicReferences) = value
def propagateCyclicReferences = this(PropagateCyclicReferences)
Copy link
Member

Choose a reason for hiding this comment

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

I think this is more appropriate as a single boolean for the entire Global, rather than one per context. Typechecking in one context can call an type competer in another context which wouldn't have this flag set.

I say that we have another thing called RecoverableCyclicReference. Unfortunately, I'm not familar with it. But it seems like it might be something that should be incorporated into this this design. Maybe when the compiler is in propagate mode, cyclic errors should be thrown as the recoverable version?

@milessabin
Copy link
Contributor Author
milessabin commented Jul 22, 2016

@retronym apropos RecoverableCyclicReference vs. CyclicReference my understanding is that a RecoverableCyclicReference indicates that a cycle has been encountered but that the typer should continue to follow alternative paths if any ... only once the alternatives are exhausted is a non-recoverable CyclicReference thrown. I think that RecoverableCyclicReference is recoverable precisely because it's just an ordinary subtype of TypeError with no special handling.

If this is the case then I think what I'm doing here is correct: the only things which result in a CyclicReference being thrown are definitely not-recoverable and should be reported in the normal case. In the maybeInitialize case I believe that it's correct to suppress the report and propagate to the try/catch at the top.

Apropos the flag being context-based vs. global: I take your point about the flag not being propagated to the context associated with a different completer. What would you recommend as a mechanism for representing the flag globally? A counter var on Global?

@adriaanm adriaanm modified the milestone: 2.12.1 Jul 26, 2016
@adriaanm
Copy link
Contributor

For now, I've assigned this to 2.12.1. We can discuss whether we can make an exception to the RC cycle rules for this one (if it can be fixed post 2.12.0, it should be), but we first need to focus on clearing the PRs blocking RC1.

@milessabin
Copy link
Contributor Author

I've done as @retronym suggested and replaced the context-based flag with a global counted one. It seems that the existing tests already exercise typechecking in one context calling a type completer in another context. I'd welcome suggestions for more tests which bear on that point.

@milessabin
Copy link
Contributor Author
milessabin commented Aug 9, 2016

FTR, the use of global mutable state makes me ... uncomfortable. But it's in good company in this area and there doesn't seem to be any straightforward way of avoiding it.

F438 @milessabin milessabin changed the title Proof of concept fix for SI-7046 Fix for SI-7046 Aug 9, 2016
@milessabin milessabin force-pushed the topic/si-7046 branch 2 times, most recently from d373130 to 3c9e1ff Compare September 7, 2016 14:53
@SethTisue
Copy link
Member

Jenkins says:

# Failed test paths (this command will update checkfiles)
partest --update-check \
  /home/jenkins/workspace/scala-2.12.x-validate-test/test/files/run/t5418.scala \
  /home/jenkins/workspace/scala-2.12.x-validate-test/test/files/run/t5418b.scala \
  /home/jenkins/workspace/scala-2.12.x-validate-test/test/files/run/t5463.scala \
  /home/jenkins/workspace/scala-2.12.x-validate-test/test/files/run/t5488-fn.scala \
  /home/jenkins/workspace/scala-2.12.x-validate-test/test/files/run/t5488.scala \
  /home/jenkins/workspace/scala-2.12.x-validate-test/test/files/run/t5500.scala \
  /home/jenkins/workspace/scala-2.12.x-validate-test/test/files/run/t5500b.scala \
  /home/jenkins/workspace/scala-2.12.x-validate-test/test/files/run/t5535.scala

@thedmitriyk
Copy link

👍 for this fix.
My project uses automatic typeclass derivation and was affected by SI-7046. The scenario was pretty trivial and not far off from known examples. This fix, as implemented in the Typelevel fork, resolved my issue brilliantly. Would love to see this merged into Lightbend Scala.

@milessabin
Copy link
Contributor Author

Rebased ...

@milessabin
Copy link
Contributor Author

Rebased.

This has been well received in TLS and merged for 2.11.9 ... is there anything more that needs to be done before merging for 2.12.1?

@adriaanm
Copy link
Contributor

I expect this will be fine, but have not had time for a final review pass. Should get to it this week.

@milessabin
Copy link
Contributor Author

Rebased.

@adriaanm adriaanm self-assigned this Nov 30, 2016
@adriaanm
Copy link
Contributor
adriaanm commented Dec 1, 2016

LGTM.

I'm a little concerned about going into the type checker from a method on Symbol to force all symbols in the enclosing package, but since this is a specialized enough use case, I guess we can see how this plays out in practice.

In future, please include design motivation in the commit message. Since this has been discussed in the PR, I guess we can make an exception.

@adriaanm adriaanm merged commit c2eb299 into scala:2.12.x Dec 1, 2016
@SethTisue SethTisue added the release-notes worth highlighting in next release notes label Dec 1, 2016
@SethTisue SethTisue changed the title Partial fix for SI-7046 SI-7046 partial fix to knownDirectSubclasses for reflection users and macro authors Dec 5, 2016
dhpiggott added a commit to dhpiggott/liquidity-service that referenced this pull request Oct 29, 2017
The ZoneValidator protocol is now:
1. Independent of the WsProtocol.
2. Still represented by the same types that they always were, as far as speakers
   of the protocol are concerned.
3. Serialized to Protobuf messages.

(3) was the goal of this change, while (2) was a constraint on the solution that
I've imposed in order to retain meaningful types throughout most of the codebase
without resorting to using ScalaPB's TypeMappers (which can only do so much).

The introduction of several new `case object Empty ...` types on the application
side is in order to enable inductive derivation of ProtoConverter instances for
the wrapper coproducts that ScalaPB generates for `oneof` message types.

The movement of ZoneValidatorMessageSerializer fromm the actor-protocol module
to the server module was done in order ensure that its compilation happens
_after_ the compilation of both the generated Protobuf protocol types and the
original protocol types (both of which are compiled in the actor-protocol
module). This was necessary because ScalaPB generates the _instances_ of the
wrapper coproducts that it creats for `oneof` message types _within_ the
companion object that it creates for the trait of which they are children.
This property of the generated code interacted badly with the way that
scalac's knownDirectSubclasses macro method works, causing errors of the form
`error: knownDirectSubclasses of Foo observed before subclass Bar registered`.

I found that commenting out the three lines in ZoneValidatorMessageSerializer
that trigger the derivation of ProtoConverter instances that depend on coproduct
ProtoConverter instances being derived was sufficient for compilation to
succeed. I then found that a second compilation attempt with them _un_commented
would somewhat suprisingly also succeed. This finding coupled with the notes on
scala/scala#5284 confirmed to me that the issue was
only happening because of the above property of the generated ScalaPB code
(ProtoConverterSpec includes test cases for coproducts which work fine, so I
knew that other derivations were working - although somewhat surprisingly it
also works if the test coproduct instances are moved to be children of
companions of their parent traits...).

Hence the workaround of simply moving the serializer instances into the server
module (which is the only place they're used anyway*). This ensures that
derivation happens _after_ the necessary compiler phases have completed on the
coproducts in question.

*ZoneEventSerializer was in a separate module because analytics used to run as a
separate deployable which built against the persistence module (but not the
server module).

Other notes:

1. There are two TODOs re. unused import warnings in the ZoneEvent and
   ZoneValidatorMessage serializers. I've left these for later resolution
   because: (a) it's taken a fair amount of time already to get this change to
   where it is, (b) the size of the diff is getting large, and (c) they aren't
   issues with the code anyway - they really seem to be an IntelliJ issue, so
   the TODOs are really about finding a fix or workaround for that.
2. ClientConnectionActor (and its Spec) are using ProtoConverter instances to
   convert WsProtocol ZoneCommands to ZoneValidatorMessage ZoneCommands. As the
   numerous comments where this occurs explain, the terms asScala and asProto
   are not appropriate here. Indeed, that's not the only way this is not ideal.
   The more preferable solution would have been to _move_ the ZoneCommands from
   WsProtocol, rather than _copy_ them. That's actually what I first attempted
   to do. However, the issue I ran into was in adapting the scala-json-rpc
   message format constructors to support wrapping an external message in a way
   that would preserve the original JSON-RPC wire format. There would have been
   a solution, but using the recently developed ProtoConverter functionality
   ended up being a much faster solution to implement (and is also less
   verbose). Since the JSON-RPC protocol is now living on borrowed time and
   should be deprecated and then removed in the near future now that all the
   prerequisite steps are complete (this being the last of them), this solution
   is reasonable enough (it will disappear when the JSON-RPC protocol is
   removed). This leads nicely on to the final point:
3. play-json removal: as part of the deprecation and later removal of the
   JSON-RPC protocol, the number of modules which include play-json as a
   dependency should decrease and eventually reach zero. For now it remains as
   a dependency of most things because I've deliberately left the application
   side ADTs with JsObject as the metadata type in order to minimise the volume
   of individual Protobuf-conversion-related commits.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release-notes worth highlighting in next release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

0