Migrate Mill to Scala 3 #2047
Replies: 8 comments 10 replies
-
Yeah, in the past some of us worked hard to get all dependencies of Mill migrated to Scala 3. Yet, as you already noticed, Mill relies on the macro system to lift the task notation into applicatives. I guess it need some brave souls to start to hack a proof of concept and show how we could do it in Scala 3. One result could also be, to clearly see what will not work, so we can start thinking about solutions. |
Beta Was this translation helpful? Give feedback.
-
I started to craft a small PoC few days ago (I am late 😅 ). I am currently succeeding and got the task name using the method (thanks Sourcecode) and dependency detection/applicative: def sayHello = T {
T.log("hello")
}
def whichDir = T {
T.wd
}
def test = T {
sayHello()
whichDir()
} test match //Print task name or the task object
case Task.Named(name, _) => println(s"Task named $name")
case task => println(s"Anonymous task: $task")
for input <- test.inputs do //Print deps or their name
input match
case Task.Named(name, _) => println(s"Dep named $name")
case task => println(s"Anonymous dep: $task") I get:
Task-specific methods also work and are not callable outside of def whichDir = T {
T.wd //OK
} T.wd [error] 22 | T.wd
[error] | ^
[error] | This method is only usable in a task or given a `TaskContext`
[error] one error found I however have some questions about Mill's internal behaviour:
def test = T {
println("test")
myDepTask()
} Is |
Beta Was this translation helpful? Give feedback.
-
Small update about the work: Last week I had time to work on the task system. The project is a very minimalist version of Mill built from scratch (still picking some Mill utils like I tried to make the project structure look as similar as possible to Mill's but again, this project is very minimalist. The migration of Scala 3 involve two main parts: task dependency detection and task-restricted methods (like Task-restricted methodsI "simply" used Dotty's [Context Functions] to restrict make some functions only usable in a task body aka given a Note: files are sampled/truncated. Task.scala enum Task[+T]:
case Literal(inputs: Seq[Task[?]], body: TaskContext => Result[T])
case Mapped[A, +B](source: Task[A], f: TaskContext => A => Result[B]) extends Task[B]
case Sequence[+A](tasks: Seq[Task[A]]) extends Task[Seq[A]]
case Named(name: String, underlying: Task[T]) TaskContext.scala @implicitNotFound("This method is only usable in a task or given a `TaskContext`")
case class TaskContext(dest: Path, logger: Logger, results: Map[Task[?], Any], values: Seq[?]): T.scala //SourceCode is used to get the name of the task (aka method defining it)
inline def apply[T](inline body: TaskContext ?=> T)(using name: sourcecode.Name): Task[T] =
Task.Named(name.value, applicative(body)) //applicative is a macro for task dependencies explained further.
def log(msg: String)(using ctx: TaskContext): Unit =
ctx.logger.log(Level.INFO, msg)
def dest(using ctx: TaskContext): Path = ctx.dest def sayHello = T {
T.log("hello world") //OK
}
T.log("hello world") //Compile-time error: This method is only usable in a task or given a `TaskContext` Task dependenciesThis part is similar to Mill's. inline def applicative[T](inline f: TaskContext ?=> T): Task[T] =
${ applicativeImpl[T]('f) }
def applicativeImpl[T](expr: Expr[TaskContext ?=> T])(using Quotes, Type[T]): Expr[Task[T]] =
import quotes.reflect.*
def recTree(tree: Tree): Expr[List[Task[?]]] = tree match
case statement: Statement => recStatement(statement)
case caseDef: CaseDef => recCase(caseDef)
case _ => '{ Nil }
def recCase(caseDef: CaseDef): Expr[List[Task[?]]] = caseDef match
case CaseDef(_, Some(guard), thenp) =>
'{ ${ recTerm(guard) } ++ ${ recTerm(thenp) } }
case CaseDef(_, None, thenp) => recTerm(thenp)
def recStatement(statement: Statement): Expr[List[Task[?]]] = statement match
case term: Term => recTerm(term)
case definition: Definition => recDef(definition)
case _ => '{ Nil }
//Other similar methods like recTerm...
'{ Task.Literal(${ recTerm(expr.asTerm) }, ctx => Result.Success(${ expr }(using ctx))) } Then for this code: def helloMessage = T {
"Hello world"
}
def whichDir = T {
T.dest
}
def both = T {
T.log(helloMessage())
T.log(s"whichDir's destination is ${whichDir()}")
}
To choose between executing the task when using That's all for the moment. Another brave soul started to create the module system. I will help them once available again. The unfinished project (not even a README!) is here. |
Beta Was this translation helpful? Give feedback.
-
I finally had free time again to work on the project. I realized that module detection/management is entirely done via Java runtime reflection which should not cause any problems in Scala 3 (indeed, I tested). Is there any other part in Mill involving macros that might be hard to migrate to Scala 3? |
Beta Was this translation helpful? Give feedback.
-
Three things that come to mind:
|
Beta Was this translation helpful? Give feedback.
-
For a nice API experience, we currently use name shadowing for the inner
Maybe, we should deprecated these |
Beta Was this translation helpful? Give feedback.
-
Is it a goal to migrate Mill to Scala 3? In that case, maybe we can convert this discussion into an issue? |
Beta Was this translation helpful? Give feedback.
-
The work is largely complete in #3369, just waiting for Mill 0.12.0 to be cut before we merge it since it breaks bincompat |
Beta Was this translation helpful? Give feedback.
-
Scala 3 was released more than a year ago and the community quickly started to adopt it (according to Scaladex library count by version).
Moving to Scala 3 has several advantages for Mill:
T.dest
) in favour of context functions.Note that the migration comes with cost:
I guess this issue will take time to be fully addressed but still a good goal.
Beta Was this translation helpful? Give feedback.
All reactions