Skip to content

Commit

Permalink
Scala 3 port (#14)
Browse files Browse the repository at this point in the history
@lihaoyi I have gone with Scala 3.5.0 here since that was the version I
have been porting Mill to.

I didn't see any testing procedure - so I used the Mill integration
tests to validate the override and scaladoc annotation inference is
working. (Also I could remove all the override modifiers I had
introduced in my Mill PR)

Note that now I made `moduledefs` a cross module in order to set the
right "species" of scala versions to build plugin against. This is
probably solved better with Cross2 but that needs to upgrade mill to
0.11.x which breaks the build in other ways I didn't want to touch

```
./millw -i resolve 'moduledefs'._
[1/1] resolve
moduledefs[2.13.14]
moduledefs[3.5.0]
```

```
./millw -i resolve 'moduledefs[2.13.14]'.plugin._
[1/1] resolve
moduledefs[2.13.14].plugin[2.13.0]
moduledefs[2.13.14].plugin[2.13.10]
moduledefs[2.13.14].plugin[2.13.11]
moduledefs[2.13.14].plugin[2.13.12]
moduledefs[2.13.14].plugin[2.13.13]
moduledefs[2.13.14].plugin[2.13.14]
moduledefs[2.13.14].plugin[2.13.1]
moduledefs[2.13.14].plugin[2.13.2]
moduledefs[2.13.14].plugin[2.13.3]
moduledefs[2.13.14].plugin[2.13.4]
moduledefs[2.13.14].plugin[2.13.5]
moduledefs[2.13.14].plugin[2.13.6]
moduledefs[2.13.14].plugin[2.13.7]
moduledefs[2.13.14].plugin[2.13.8]
moduledefs[2.13.14].plugin[2.13.9]
```

```
./millw -i resolve 'moduledefs[3.5.0]'.plugin._
[1/1] resolve
moduledefs[3.5.0].plugin[3.5.0]
```
  • Loading branch information
lihaoyi authored Aug 21, 2024
2 parents 236b00a + 6450447 commit 0c58829
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 14 deletions.
6 changes: 6 additions & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@ newlines.source = keep

runner.dialect = scala213

fileOverride {
"glob:**/src-3/**.scala" {
runner.dialect = scala3
rewrite.scala3.removeOptionalBraces.enabled = false
}
}
48 changes: 34 additions & 14 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@ import mill.eval.Evaluator
import mill.define.{SelectMode, Command}

object Settings {
val version = "0.10.9"
val version = "0.10.10"
val pomOrg = "com.lihaoyi"
val githubOrg = "com-lihaoyi"
val githubRepo = "mill-moduledefs"
val projectUrl = s"https://github.com/${githubOrg}/${githubRepo}"
}

object Deps {
val scalaVersions = 0.to(14).map(v => "2.13." + v)
val scalaVersion = scalaVersions.reverse.head
def scalaCompiler(scalaVersion: String) = ivy"org.scala-lang:scala-compiler:${scalaVersion}"
val scala2Versions = 0.to(14).map(v => "2.13." + v)
val scala3Versions = Seq("3.5.0")
val scalaAllVersions = Map(scala2Versions.last -> scala2Versions, scala3Versions.last -> scala3Versions)
def scalaCompiler(scalaVersion: String) =
if (scalaVersion.startsWith("3.")) ivy"org.scala-lang::scala3-compiler:${scalaVersion}"
else ivy"org.scala-lang:scala-compiler:${scalaVersion}"
val sourcecode = ivy"com.lihaoyi::sourcecode:0.3.0"
}

Expand All @@ -35,26 +38,43 @@ trait ModuledefsBase extends ScalaModule with PublishModule {
)
)
override def javacOptions = Seq("-source", "1.8", "-target", "1.8", "-encoding", "UTF-8")
override def scalacOptions = T {
super.scalacOptions() ++ (
if (scalaVersion().startsWith("3.")) Seq("-Yexplicit-nulls", "-no-indent")
else Seq.empty
)
}
}

object moduledefs extends ModuledefsBase {
object moduledefs extends Cross[ModuleDefsCross](Deps.scalaAllVersions.keys.toSeq: _*)
class ModuleDefsCross(override val crossScalaVersion: String) extends CrossScalaModule
with ModuledefsBase { outer =>
override def artifactName = "mill-" + super.artifactName()
override def scalaVersion = Deps.scalaVersion
override def ivyDeps = Agg(
Deps.scalaCompiler(scalaVersion()),
Deps.sourcecode
)
override def ivyDeps = {
val sv = crossScalaVersion
Agg(Deps.sourcecode) ++
(if (sv.startsWith("2.")) Agg(Deps.scalaCompiler(sv)) else Agg.empty)
}

object plugin extends Cross[PluginCross](Deps.scalaVersions: _*)
object plugin extends Cross[PluginCross](Deps.scalaAllVersions(crossScalaVersion): _*)
class PluginCross(override val crossScalaVersion: String) extends CrossScalaModule
with ModuledefsBase {
override def artifactName = "scalac-mill-" + super.artifactName()
override def moduleDeps = Seq(moduledefs)
override def artifactName = "scalac-mill-moduledefs-plugin"
// ^^ TODO: cant use `"scalac-mill-" + super.artifactName()` here
// because it includes the crossScalaVersion of `moduledefs`
// could be addressed with Cross2 from mill 0.11.x
override def moduleDeps = Seq(moduledefs(outer.crossScalaVersion))
override def crossFullScalaVersion = true
override def ivyDeps = Agg(
Deps.scalaCompiler(scalaVersion()),
Deps.scalaCompiler(crossScalaVersion),
Deps.sourcecode
)

override def resources = T.sources {
super.resources() ++ scalaVersionDirectoryNames.map(dir =>
PathRef(millSourcePath / s"resources-$dir")
)
}
}
}

Expand Down
1 change: 1 addition & 0 deletions moduledefs/plugin/resources-3/plugin.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pluginClass=mill.moduledefs.AutoOverridePluginDotty
113 changes: 113 additions & 0 deletions moduledefs/plugin/src-3/AutoOverridePluginDotty.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package mill.moduledefs

import scala.collection.mutable.ListBuffer

import dotty.tools.dotc.plugins.{StandardPlugin, PluginPhase}
import dotty.tools.dotc.core.Contexts.{Context, ctx}
import dotty.tools.dotc.core.Comments.docCtx
import dotty.tools.dotc.core.Symbols.Symbol
import dotty.tools.dotc.core.Symbols.ClassSymbol
import dotty.tools.dotc.core.Symbols.requiredClass
import dotty.tools.dotc.core.Names.termName
import dotty.tools.dotc.core.Flags
import dotty.tools.dotc.report
import dotty.tools.dotc.core.Constants.Constant
import dotty.tools.dotc.core.Annotations.Annotation
import dotty.tools.dotc.ast.tpd.{Template, ValDef, DefDef, Literal, NamedArg}
import dotty.tools.dotc.util.Spans.Span

class AutoOverridePluginDotty extends StandardPlugin {

override def initialize(options: List[String])(using Context): List[PluginPhase] =
List(EnableScaladocAnnotation(), AutoOverride())

val name = "auto-override-plugin"
val description = "automatically inserts `override` keywords for you"

/** basically we override each kind of definition to copy over its doc comment to an annotation. */
private class EnableScaladocAnnotation extends PluginPhase {

override val phaseName: String = "EmbedScaladocAnnotation"

override val runsAfter: Set[String] = Set("posttyper")
override val runsBefore: Set[String] = Set("pickler") // TODO: should the annotation be in TASTY?

private var _ScalaDocAnnot: ClassSymbol | Null = null
private def ScalaDocAnnot(using Context): ClassSymbol = {
val local = _ScalaDocAnnot
if local == null then {
val sym = requiredClass(AutoOverridePluginDotty.scaladocAnnotationClassName)
_ScalaDocAnnot = sym
sym
} else local
}
private lazy val valueName = termName("value")

private def cookComment(sym: Symbol, span: Span)(using Context): Unit = {
for
docCtx <- ctx.docCtx
comment <- docCtx.docstring(sym)
do {
val text = NamedArg(valueName, Literal(Constant(comment.raw))).withSpan(span)
sym.addAnnotation(Annotation(ScalaDocAnnot, text, span))
}
}

override def prepareForTemplate(tree: Template)(using Context): Context = {
cookComment(tree.symbol, tree.span)
ctx
}

override def prepareForValDef(tree: ValDef)(using Context): Context = {
cookComment(tree.symbol, tree.span)
ctx
}

override def prepareForDefDef(tree: DefDef)(using Context): Context = {
cookComment(tree.symbol, tree.span)
ctx
}

}

/** This phase automatically adds the override annotation to methods that require one. */
private class AutoOverride extends PluginPhase {

override val runsAfter = Set("posttyper")
override val runsBefore = Set("crossVersionChecks") // this is where override checking happens

val phaseName = "auto-override"

private var _Cacher: ClassSymbol | Null = null
private def Cacher(using Context): ClassSymbol = {
val local = _Cacher
if local == null then {
val sym = requiredClass(AutoOverridePluginDotty.cacherClassName)
_Cacher = sym
sym
} else local
}

private def isCacher(owner: Symbol)(using Context): Boolean =
if owner.isClass then owner.asClass.baseClasses.exists(_ == Cacher)
else false

override def prepareForDefDef(d: DefDef)(using Context): Context = {
val sym = d.symbol
if sym.allOverriddenSymbols.count(!_.is(Flags.Abstract)) >= 1
&& !sym.is(Flags.Override)
&& isCacher(sym.owner)
then
sym.flags = sym.flags | Flags.Override
ctx
}
}

}

object AutoOverridePluginDotty {

private val cacherClassName = "mill.moduledefs.Cacher"
private val scaladocAnnotationClassName = "mill.moduledefs.Scaladoc"

}
File renamed without changes.
58 changes: 58 additions & 0 deletions moduledefs/src-3/Cacher.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package mill.moduledefs

import scala.collection.mutable
import scala.quoted.*

trait Cacher {
private lazy val cacherLazyMap = mutable.Map.empty[sourcecode.Enclosing, Any]

protected def cachedTarget[T](t: => T)(implicit c: sourcecode.Enclosing): T = synchronized {
cacherLazyMap.getOrElseUpdate(c, t).asInstanceOf[T]
}
}

object Cacher {
private def withMacroOwner[T](using Quotes)(op: quotes.reflect.Symbol => T): T = {
import quotes.reflect.*

// In Scala 3, the top level splice of a macro is owned by a symbol called "macro" with the macro flag set,
// but not the method flag.
def isMacroOwner(sym: Symbol)(using Quotes): Boolean =
sym.name == "macro" && sym.flags.is(Flags.Macro | Flags.Synthetic) && !sym.flags.is(
Flags.Method
)

def loop(owner: Symbol): T =
if owner.isPackageDef || owner == Symbol.noSymbol then
report.errorAndAbort(
"Cannot find the owner of the macro expansion",
Position.ofMacroExpansion
)
else if isMacroOwner(owner) then op(owner.owner) // Skip the "macro" owner
else loop(owner.owner)

loop(Symbol.spliceOwner)
}

def impl0[T: Type](using Quotes)(t: Expr[T]): Expr[T] = withMacroOwner { owner =>
import quotes.reflect.*

val CacherSym = TypeRepr.of[Cacher].typeSymbol

val ownerIsCacherClass =
owner.owner.isClassDef &&
owner.owner.typeRef.baseClasses.contains(CacherSym)

if (ownerIsCacherClass && owner.flags.is(Flags.Method)) {
val enclosingCtx = Expr.summon[sourcecode.Enclosing].getOrElse(
report.errorAndAbort("Cannot find enclosing context", Position.ofMacroExpansion)
)

val thisSel = This(owner.owner).asExprOf[Cacher]
'{ $thisSel.cachedTarget[T](${ t })(using $enclosingCtx) }
} else report.errorAndAbort(
"T{} members must be defs defined in a Cacher class/trait/object body",
Position.ofMacroExpansion
)
}
}

0 comments on commit 0c58829

Please sign in to comment.