DynamicCompilation

//I am trying to dynamically define , compile and access case classes that are //nested
//

package org.example

import scala.reflect.runtime.currentMirror
import scala.tools.reflect.ToolBox

object DynamicCompilation {

def compileCaseClasses(code: String): Option[ClassLoader] = {
try {
val tb = currentMirror.mkToolBox()
val compiled = tb.compile(tb.parse(code))
// Get class loader from the compiled object
val classLoader = compiled.getClass.getClassLoader
// Print the class loader information
println(s"Class loader being compiled is : $classLoader")
Some(classLoader)
} catch {
case e: Throwable =>
println(s"Compilation Error: ${e.getMessage}“)
e.printStackTrace()
None
}
}
def getClassByName(classLoader: ClassLoader, className: String): Option[Class[_]] = {
try {
Some(classLoader.loadClass(s"org.example.$className”)) // Assuming package org.example
} catch {
case e: ClassNotFoundException =>
println(s"Class org.example.$className not found: ${e.getMessage}")
None
}
}

def main(args: Array[String]): Unit = {
val code = “”"

{
case class Address(
city: Option[String],
country: Option[String],
street: Option[String]
)
case class Person(
address: Option[Address],
age: Option[Long],
name: Option[String]
)
case class Root(
people: Option[Seq[Person]]
)
}
“”"

compileCaseClasses(code) match {
  case Some(classLoader) =>
    getClassByName(classLoader, "org.example.Address") match {
      case Some(addressClass) =>
        println(s"Successfully compiled Address class: $addressClass")
      case None => println("Could not find Address class.")
    }
    getClassByName(classLoader, "Person") match {
      case Some(personClass) =>
        println(s"Successfully compiled Person class: $personClass")
      // ... (Example of creating an instance and accessing fields)
      case None => println("Could not find Person class.")
    }
    getClassByName(classLoader, "Root") match {
      case Some(rootClass) =>
        println(s"Successfully compiled Root class: $rootClass")
      case None => println("Could not find Root class.")
    }
  case None =>
    println("Compilation failed.")
}

}
}

May I ask what your use case is? This sort of dynamic compilation is unusual in the Scala world, and access is inherently going to be challenging, since the types don’t exist at the primary compile time.

That said: what’s the question? It’s not obvious what’s going on here, or what problems you’re hitting.

I want to access the case classes I just compiled on the fly. especially if I used one of them to apply type safety to a dataframe to create a dataset in spark

val ds = df.as[Address]

Wouldn’t macros make more sense for your use case? There is also a Scala 3 library that, by my understanding does a similar thing GitHub - VirtusLab/iskra: Typesafe wrapper for Apache Spark DataFrame API

As for dynamic compilation we do that for mdoc but it’s not possible to get those compiled types reliably really, but rather have them behind a trait. Alternatively, you could potentially add those compiled classes to filesystem and the classpath, but that might break easily

1 Like

If I’m understanding what you’re trying to do (which I might not), it’s impossible. If, say, Address is only defined at runtime, then

val ds = df.as[Address]

can’t compile, since Address doesn’t exist at compile-time.

(That’s not a Scala limitation – it’s just inherent in the different between compile-time and runtime.)

I agree with @tgodzik that macros might work for your needs, but that’s because it’s a compile-time operation. If stuff really can’t be defined until runtime, then it’s not really possible to use it in a typesafe way like that.

1 Like

An advanced technique is too use runtime multistaged programming

https://docs.scala-lang.org/scala3/reference/metaprogramming/staging.html

Good point – I don’t think I’ve seen that used in the wild yet, but it’s an intriguing new technique.

Based on the docs, though, I think it requires knowing and stating the types upfront, even if the implementation is generated at runtime – that’s how it gets around my “impossible” statement above. (Indeed, it’s an interesting illustration of the difference between compile-time types and runtime classes.)

And it isn’t obvious to me whether this can work when starting out with a String at compile time. (Might be possible; I don’t know the new metaprogramming systems deeply enough yet.)

2 Likes

How can I run generated code during script runtime? Scala 2

How to compile and execute scala code at run-time in Scala3? Scala 3

Oh! I missed that. No, pretty sure you cannot start out with a string and use that. Gotta spin up a full compiler for that.

As i understand, the meta programming of scala 3 can be narrowed to only what’s required for the dynamic behavior specified. Not really what happens as far as i know.

@hmcbride
Correct class loader is tb.mirror.classLoader, not compiled.getClass.getClassLoader.

Correct way to work with toolbox-generated classes is with tb.define:

using quasiquotes:

import scala.reflect.runtime.universe.{ClassDef, Quasiquote}

val addressSymbol = tb.define(
  q"""
      case class Address(
                          city: Option[String],
                          country: Option[String],
                          street: Option[String]
                        )
  """.asInstanceOf[ClassDef]
).asClass

val personSymbol = tb.define(
  q"""
    case class Person(
                       address: Option[$addressSymbol],
                       age: Option[Long],
                       name: Option[String]
                     )
  """.asInstanceOf[ClassDef]
).asClass

val rootSymbol = tb.define(
  q"""
    case class Root(
                     people: Option[Seq[$personSymbol]]
                   )
  """.asInstanceOf[ClassDef]
).asClass

val addressClass = tb.mirror.runtimeClass(addressSymbol)
val personClass = tb.mirror.runtimeClass(personSymbol)
val rootClass = tb.mirror.runtimeClass(rootSymbol)

or parsing strings:

val addressSymbol = tb.define(tb.parse(
  """case class Address(
    |                     city: Option[String],
    |                     country: Option[String],
    |                     street: Option[String]
    |                  )""".stripMargin
).asInstanceOf[ClassDef])

val personSymbol = tb.define(tb.parse(
  s"""case class Person(
     |                    address: Option[${addressSymbol.fullName}],
     |                    age: Option[Long],
     |                    name: Option[String]
     |                 )""".stripMargin
).asInstanceOf[ClassDef])

val rootSymbol = tb.define(tb.parse(
  s"""case class Root(
     |                  people: Option[Seq[${personSymbol.fullName}]]
     |               )""".stripMargin
).asInstanceOf[ClassDef])

val toolboxClassLoader = tb.mirror.classLoader
val addressClass = toolboxClassLoader.loadClass(addressSymbol.fullName)
val personClass = toolboxClassLoader.loadClass(personSymbol.fullName)
val rootClass = toolboxClassLoader.loadClass(rootSymbol.fullName)

The package will be not org.example but something like __wrapper$1$b1d11b2e2a4c40bab5387e1f253f30fb.

1 Like

@hmcbride If you want to compile all classes at once, accessing them will be trickier

import scala.reflect.runtime.currentMirror
import scala.tools.reflect.ToolBox
import scala.jdk.CollectionConverters._

object DynamicCompilation {

  def compileCaseClasses(code: String): Option[ClassLoader] = {
    try {
      val tb = currentMirror.mkToolBox()
      val compiled = tb.compile(tb.parse(code))
      val classLoader = tb.mirror.classLoader
      println(s"Class loader being compiled is : $classLoader")
      Some(classLoader)
    } catch {
      case e: Throwable =>
        println(s"Compilation Error: ${e.getMessage}")
        e.printStackTrace()
        None
    }
  }

  // javaOptions += "--add-opens=java.base/java.lang=ALL-UNNAMED", fork := true
  // java 11
    val classLoaderClassesField = classOf[ClassLoader].getDeclaredField("classes")
    classLoaderClassesField.setAccessible(true)
  // java 17
//  val classGetDeclaredFields0Method = classOf[Class[?]].getDeclaredMethod("getDeclaredFields0", classOf[Boolean])
//  classGetDeclaredFields0Method.setAccessible(true)
//  val classLoaderClassesField = classGetDeclaredFields0Method.invoke(classOf[ClassLoader], false)
//    .asInstanceOf[Array[java.lang.reflect.Field]]
//    .find(_.getName == "classes").get
//  classLoaderClassesField.setAccessible(true)

  def getClassByName(classLoader: ClassLoader, className: String): Option[Class[_]] = {
    try {
      val classes = classLoaderClassesField.get(classLoader)
        .asInstanceOf[java.util.Vector[Class[?]]] // java 11
        // .asInstanceOf[java.util.ArrayList[Class[?]]] // java 17
        .asScala
      classes
        .find(_.getName.contains(className)).map { clazz =>
          // ...$2$ is a companion object, ...$1 is a class
          classLoader.loadClass(clazz.getName.replace(className + "$2$", className + "$1"))
        }
    } catch {
      case e: ClassNotFoundException =>
        println(s"Class $className not found: ${e.getMessage}")
        None
    }
  }

  def main(args: Array[String]): Unit = {
    val code = """
    {
      case class Address(
                          city: Option[String],
                          country: Option[String],
                          street: Option[String]
                        )
      case class Person(
                         address: Option[Address],
                         age: Option[Long],
                         name: Option[String]
                       )
      case class Root(
                       people: Option[Seq[Person]]
                     )
    }
    """

    compileCaseClasses(code) match {
      case Some(classLoader) =>
        getClassByName(classLoader, "Address") match {
          case Some(addressClass) =>
            println(s"Successfully compiled Address class: $addressClass")
          case None => println("Could not find Address class.")
        }
        getClassByName(classLoader, "Person") match {
          case Some(personClass) =>
            println(s"Successfully compiled Person class: $personClass")
          case None => println("Could not find Person class.")
        }
        getClassByName(classLoader, "Root") match {
          case Some(rootClass) =>
            println(s"Successfully compiled Root class: $rootClass")
          case None => println("Could not find Root class.")
        }
      case None =>
        println("Compilation failed.")
    }
  }
}

//Class loader being compiled is : scala.reflect.internal.util.AbstractFileClassLoader@62df0ff3
//Successfully compiled Address class: class __wrapper$1$40b3bbb674b04663934483404ecee51d.__wrapper$1$40b3bbb674b04663934483404ecee51d$Address$1
//Successfully compiled Person class: class __wrapper$1$40b3bbb674b04663934483404ecee51d.__wrapper$1$40b3bbb674b04663934483404ecee51d$Person$1
//Successfully compiled Root class: class __wrapper$1$40b3bbb674b04663934483404ecee51d.__wrapper$1$40b3bbb674b04663934483404ecee51d$Root$1
1 Like

this program dynamically compiles code for Scala 3

import dotty.tools.dotc.Driver
import java.net.{URL, URLClassLoader}
import java.nio.file.{Files, Path}
import java.io.File

object DynamicCompiler:

  private val driver = Driver()

  def compile(code: String, className: String, outputDir: Path): Boolean =
    val sourceFile = Files.createTempFile(className, ".scala").toFile
    sourceFile.deleteOnExit()
    Files.writeString(sourceFile.toPath, code)

    val args = Array(
       sourceFile.getAbsolutePath,
       "-d",
       outputDir.toAbsolutePath.toString,
       "-classpath",
       System.getProperty("java.class.path")
    )

    val result = driver.process(args)
    !result.hasErrors

  def loadClass(className: String, classDir: Path): Option[Class[?]] =
    val loader = URLClassLoader(Array(classDir.toUri.toURL), getClass.getClassLoader)
    try Some(loader.loadClass(className))
    catch case _: ClassNotFoundException => None

  def main(args: Array[String]): Unit =
    val outDir = Files.createTempDirectory("scala3-compiled")
    val objectName = "DynamicRoot"
    val className = "Root"

    val code =
      s"""
         |case class Address(city: Option[String], country: Option[String], street: Option[String])
         |case class Person(address: Option[Address], age: Option[Long], name: Option[String])
         |case class $className(people: Option[Seq[Person]])
         |object $objectName
         |""".stripMargin

    if compile(code, objectName, outDir) then
      println(s"Compilation succeeded. Output in: $outDir")
      loadClass(objectName, outDir) match
        case Some(objCls) => println(s"Loaded object class: $objCls")
        case None         => println(s"Failed to load object class: $objectName")

      loadClass(className, outDir) match
        case Some(rootCls) => println(s"Loaded case class: $rootCls")
        case None          => println(s"Failed to load case class: $className")
    else
      println("Compilation failed.")

2 Likes