-
Notifications
You must be signed in to change notification settings - Fork 121
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
Invalidate macro source when its dependency changes #1282
Conversation
Previously the classes of an internal, project dependencies was missing from the classpath when calling the "run" command.
@Friendseeker you've been doing invalidation stuff recently (which is noticed and much appreciated!) — perhaps you'd like to do a little reviewing on this one? |
Thank you for considering me for this review; I really appreciate it! I must admit, my experience with Scala macros is quite limited. However, I'm willing to take a closer look at the PR and provide my comments where I can. |
internal/zinc-core/src/main/scala/sbt/internal/inc/IncrementalCommon.scala
Outdated
Show resolved
Hide resolved
@dwijnand I've reviewed the changes in your PR and understand the modifications you've made. However, to get a complete picture, could you please provide some insight into the reasoning behind this approach? It appears to be a quick fix, and I'm curious if there were specific constraints or considerations that led to this solution. |
I went with the first fix that worked, not being able to tell where I should look to slot this fix in. In particular I ended up moving my change to "initial changes" from |
@@ -477,15 +478,22 @@ private[inc] abstract class IncrementalCommon( | |||
if (secondClassInvalidation.nonEmpty) | |||
log.debug(s"Invalidated due to generated class file collision: ${secondClassInvalidation}") | |||
|
|||
val newInvalidations = (firstClassInvalidation -- recompiledClasses) ++ secondClassInvalidation | |||
// Invalidate macros that transitively depend on any of the recompiled classes | |||
val thirdClassInvalidation = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM in terms of transitive dependency correctness. There's probably still some edge cases left but this did cover the edge cases I can think of.
Preferably someone can do a performance review. If the performance is acceptable then I personally think this is good to go. If not, we might need to think of a way to optimize computation of thirdClassInvalidation
(e.g. 2-way BFS, some tweaked version of union find...), or fall back to initial approach of checking at invalidateInitial
only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think computing thirdClassInvalidation
would impact performance, because invalidateClassesInternally
already calls transitiveDeps
- although there it operates on the classes in changes
, which is a subset of recompiledClasses
we're operating on here (because we care about classes that changed in behaviour, not just those that changed in API). Let me know if I'm missing something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation! Overall LGTM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the contribution (and thanks to reviewers as well)! I'm happy to merge this as-is, but my only ask would be to elaborate the motivation for this PR in the PR description as "problem such and such doesn't work. \n\n solution here's the approach to fix it." section.
I think I found an edge case. If transitive dependencies of Macro lives in different projects, then To address this issue, when performing dependency traversal, we need to traverse through both internal dependency (dependency within a project) and external dependency (project to project dependency), fetching new analyses during traversal when necessary. We can probably implement this logic via creating a new version of We can either address the edge case in this PR, or merge this PR first and create a new PR to address this edge case. I think either is fine. |
I think we should defer this part, perhaps to never. The first reason is because I think it's common practice in projects to have a subproject dedicated for a macro, and that subproject doesn't have any dependencies. And the reason for that (IIRC and if things haven't changed) because declaring a macro dependency only puts the macro jar on the compilation classpath so it's present during macro expansion, without any of it (potential) dependencies - thus only scala-library, scala-reflect and the jdk of course. Now technically a project could develop that jar as separate subprojects and then assemble it into one jar, and then could suffer this problem, but... perhaps that reality doesn't exist and even if so, it's "only" incremental builds. The second reason is I can't find a good way to implement that. You mentioned that to address this you need to traverse external dependencies. That won't work. The intra-subproject solution we have here takes the "recompiledClasses" (which might or might not have API changes), e.g. I'll detail that in the PR description and in the code. |
Thanks for the explanation. Let's defer it then! |
Invalidate macro classes that transitively depend on any of the recompiled classes
The macro expansion tree can depend on the behavioural change of any upstream code change,
not just API changes, so correctness requires aggressive recompilation of downstream classes.
Technically the macro doesn't need to be recompiled - it's the classes downstream of the macro
that need to be recompiled, so that the macros can be re-expanded. But recompiling is the most
straightforward way to signal any classes downstream of the macro that they need to recompile.
Also, note, that this solution only works for behavioural changes in sources within the same
subproject as the macro. Changes in behaviour in upstream subprojects don't cause downstream
macro classes to recompile - because downstream projects only have visibility of the upstream
API, and if it changed, which is insufficient, and upstream projects have no other way than
their API to signal to downstream.