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

Make mill.define.Module a trait to allow abstract/virtual module lazy vals to be traits rather than classes #2536

Merged
merged 23 commits into from May 22, 2023

Conversation

lihaoyi
Copy link
Member

@lihaoyi lihaoyi commented May 20, 2023

The basic problem is that when Module was a class, and you had trait FooModule extends Module, Java reflection would not see that FooModule <: Module, and thus Reflect.reflectNestedObjects0[Module] which relies on java reflect would not detect def foo: FooModule as a module. This is because trait extends class is a Scala compiler concept, and does not exist in the JVM bytecode.

However, we do need Module to be a class, because we rely on implicit parameters to propagate context down the module tree. traits cannot take implicit parameters

The solution here is to make Module a trait, and make it inherit from a Module.BaseClass class that takes the implicit parameter and passes it to Module. Thus whenever we do reflection to find FooModule <: Module, it behaves expected, and we are still able to receive implicit parameters via Module.BaseClass

One side effect is that we need a way to mark things like JavaModule#zincWorker as not a child module. Previously, we skipped such modules during target resolution due to the buggy behavior above. Now, such modules get picked up, which is not what we want: in the common case, zincWorker is just a reference to the shared external module mill.scalalib.ZincWorkerModule, and is not a child module of each individual JavaModule. To do this, I introduce a mill.define.ModuleRef wrapper that simply provides a thin layer of indirection, letting us refer to the module via zincWorker() but blocking identification as a child-module since ModuleRef is not a sub-type of Module.

Tested via the mill.resolve.ResolveTests.overridenModule tests, which now pass when the abstract/overriden module is a trait, whereas previously they required that it be a class. Should also fix the flakiness we've been seeing in mill.resolve.ResolveTests.overridenModule.0.

@lihaoyi lihaoyi changed the title Make mill.define.Module a trait to allow abstract module vals to work without classes Make mill.define.Module a trait to allow abstract module lazy vals to work without classes May 20, 2023
@lihaoyi lihaoyi changed the title Make mill.define.Module a trait to allow abstract module lazy vals to work without classes Make mill.define.Module a trait to allow abstract/virtual module lazy vals to work without classes May 21, 2023
@lihaoyi lihaoyi changed the title Make mill.define.Module a trait to allow abstract/virtual module lazy vals to work without classes Make mill.define.Module a trait to allow abstract/virtual module lazy vals to be traits rather than classes May 21, 2023
@lihaoyi
Copy link
Member Author

lihaoyi commented May 21, 2023

This is binary-incompatible with 0.11.0-M9, but probably worth getting in before 0.11.0-final. It fixes I believe the last edge case where someone would be forced to define a Module as a class, and not for any good reason but rather due to obscure edge case behavior in how the Scala compiler outputs Java classfiles

@lihaoyi lihaoyi marked this pull request as ready for review May 21, 2023 05:23
@lihaoyi lihaoyi requested review from lolgab and lefou May 21, 2023 05:23
@@ -171,9 +171,9 @@ trait ScoverageModule extends ScalaModule { outer: ScalaModule =>
)
}

val scoverage: ScoverageData = new ScoverageData(implicitly)
lazy val scoverage = new ScoverageData {}
Copy link
Member

Choose a reason for hiding this comment

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

Is this only to be lazy, or is it fixing some early initialization stuff? If this is a (required) pattern, we should probably document it in Module.

Copy link
Member Author

Choose a reason for hiding this comment

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

It doesn't need to be lazy, but I thought that's probably what we should tell people to do. After all, objects are lazy, so using lazy val for these would avoid unnecessarily changing the evaluation model. It also helps avoid NullPointerExceptions from popping up in surprising places, especially once multiple trait inheritance and overriding is involved. Not a problem with this specific definition, but definitely a potential problem in general

* module as a child-module of the first.
*/
case class ModuleRef[+T <: mill.define.Module](t: T) {
def apply() = t
Copy link
Member

Choose a reason for hiding this comment

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

Explicit return type would be nice. Wouldn't be a normal (non-case) class enough?

Copy link
Member Author

Choose a reason for hiding this comment

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

It would. It would just be a bit more boilerplate to define

Copy link
Member

@lefou lefou left a comment

Choose a reason for hiding this comment

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

I like this change. It especially improves the case where one needs to define some inner workers which typically work out of the box but need some customization in some rare cases, like ScoverageData. I think we should be clear whether the holder needs to be lazy to work correctly (avoid nulls) or if this is only for optimization.

@lihaoyi lihaoyi merged commit 24de8f4 into com-lihaoyi:main May 22, 2023
32 of 33 checks passed
@lefou lefou added this to the 0.11.0-M10 milestone May 22, 2023
* Used to refer to a module from another module without including the target
* module as a child-module of the first.
*/
case class ModuleRef[+T <: mill.define.Module](t: T) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Small suggestion, but could you be more meaningful with some of these names? For example in mill-scip I have some diffs now that look like

-              val zincWorker = sm.zincWorker.worker()
+              val zincWorker = sm.zincWorker.t.worker()

And it's really odd to have just t here.

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, the envisioned usage is to use the def apply() = t.

-              val zincWorker = sm.zincWorker.worker()
+              val zincWorker = sm.zincWorker().worker()

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ahh duh,that makes more sense indeed.

Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't t be private then?

Copy link
Member

Choose a reason for hiding this comment

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

Doesn't it invalidate the case class contract then? E.g. when pattern matching.

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 doesn't need to be a case class i think, just need a trait with an apply method

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.

None yet

4 participants