Skip to content

[DRAFT] Allow customisation of metaprogramming via scalac flags #12039

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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ trait CommonScalaSettings { self: Settings.SettingGroup =>
/** Other settings */
val encoding: Setting[String] = StringSetting("-encoding", "encoding", "Specify character encoding used by source files.", Properties.sourceEncoding, aliases = List("--encoding"))
val usejavacp: Setting[Boolean] = BooleanSetting("-usejavacp", "Utilize the java.class.path in classpath resolution.", aliases = List("--use-java-class-path"))
val compileTimeEnv: Setting[List[String]] = MultiStringSetting("-E", "key[=value]", "Options to pass to the metaprogramming environment. e.g. -Ecom.example.app.mode=RELEASE")

/** Plugin-related setting */
val plugin: Setting[List[String]] = MultiStringSetting ("-Xplugin", "paths", "Load a plugin from each classpath.")
Expand Down
8 changes: 8 additions & 0 deletions compiler/src/dotty/tools/dotc/config/Settings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,14 @@ object Settings {

def matches(argName: String) = (name :: aliases).exists(_ == argName)

// TODO Do properly
if name == "-E" && prefix.isEmpty && arg.startsWith(name) then
val s = arg.drop(name.length)
if s.isEmpty || s.startsWith("=") then
return state // Upstream will report as a bad option
else
return update(s :: Nil, args)

if (prefix != "" && arg.startsWith(prefix))
doSet(arg drop prefix.length)
else if (prefix == "" && matches(arg.takeWhile(_ != ':')))
Expand Down
32 changes: 32 additions & 0 deletions compiler/src/dotty/tools/dotc/core/CompileTimeEnvMap.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package dotty.tools.dotc.core

import dotty.tools.dotc.config.ScalaSettings
import dotty.tools.dotc.config.Settings.Setting.value
import Contexts._

// TODO doc
final case class CompileTimeEnvMap(env: Map[String, String]) {
def apply(key: String): Option[String] =
env.get(key)
}

object CompileTimeEnvMap {

def fromSettings(using Context): CompileTimeEnvMap = {
var m = Map.empty[String, String]

for (s <- ctx.settings.compileTimeEnv.value)
val i = s.indexOf('=')
if i > 0 then
// -Ekey=value
val key = s.take(i)
val value = s.drop(i + 1)
m = m.updated(key, value)
else if i < 0 then
// -Ekey
val key = s
if !m.contains(key) then m = m.updated(key, "")

new CompileTimeEnvMap(m)
}
}
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Contexts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,9 @@ object Contexts {
/** Is the explicit nulls option set? */
def explicitNulls: Boolean = base.settings.YexplicitNulls.value

lazy val compileTimeEnvMap: CompileTimeEnvMap =
CompileTimeEnvMap.fromSettings

/** Initialize all context fields, except typerState, which has to be set separately
* @param outer The outer context
* @param origin The context from which fields are copied
Expand Down
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -226,14 +226,15 @@ class Definitions {
@tu lazy val ScalaXmlPackageClass: Symbol = getPackageClassIfDefined("scala.xml")

@tu lazy val CompiletimePackageClass: Symbol = requiredPackage("scala.compiletime").moduleClass
@tu lazy val Compiletime_codeOf: Symbol = CompiletimePackageClass.requiredMethod("codeOf")
@tu lazy val Compiletime_codeOf : Symbol = CompiletimePackageClass.requiredMethod("codeOf")
@tu lazy val Compiletime_erasedValue : Symbol = CompiletimePackageClass.requiredMethod("erasedValue")
@tu lazy val Compiletime_uninitialized: Symbol = CompiletimePackageClass.requiredMethod("uninitialized")
@tu lazy val Compiletime_error : Symbol = CompiletimePackageClass.requiredMethod(nme.error)
@tu lazy val Compiletime_requireConst : Symbol = CompiletimePackageClass.requiredMethod("requireConst")
@tu lazy val Compiletime_constValue : Symbol = CompiletimePackageClass.requiredMethod("constValue")
@tu lazy val Compiletime_constValueOpt: Symbol = CompiletimePackageClass.requiredMethod("constValueOpt")
@tu lazy val Compiletime_summonFrom : Symbol = CompiletimePackageClass.requiredMethod("summonFrom")
@tu lazy val Compiletime_envGetOrNull : Symbol = CompiletimePackageClass.requiredMethod("envGetOrNull")
@tu lazy val CompiletimeTestingPackage: Symbol = requiredPackage("scala.compiletime.testing")
@tu lazy val CompiletimeTesting_typeChecks: Symbol = CompiletimeTestingPackage.requiredMethod("typeChecks")
@tu lazy val CompiletimeTesting_typeCheckErrors: Symbol = CompiletimeTestingPackage.requiredMethod("typeCheckErrors")
Expand Down
26 changes: 22 additions & 4 deletions compiler/src/dotty/tools/dotc/typer/Inliner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -665,19 +665,37 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) {
case _ => EmptyTree
}

private def extractConstantValue(t: Tree): Option[Any] =
t match
case ConstantValue(v) => Some(v)
case Inlined(_, Nil, Typed(ConstantValue(v), _)) => Some(v)
case _ => None

private def reportErrorConstantValueRequired(arg: Tree): Unit =
report.error(em"expected a constant value but found: $arg", arg.srcPos)

/** The Inlined node representing the inlined call */
def inlined(sourcePos: SrcPos): Tree = {

// Special handling of `requireConst` and `codeOf`
// Special handling of `requireConst`, `codeOf`, `compileTimeEnv`
callValueArgss match
case (arg :: Nil) :: Nil =>
if inlinedMethod == defn.Compiletime_requireConst then
arg match
case ConstantValue(_) | Inlined(_, Nil, Typed(ConstantValue(_), _)) => // ok
case _ => report.error(em"expected a constant value but found: $arg", arg.srcPos)
if extractConstantValue(arg).isEmpty then
reportErrorConstantValueRequired(arg)
return Literal(Constant(())).withSpan(sourcePos.span)
else if inlinedMethod == defn.Compiletime_codeOf then
return Intrinsics.codeOf(arg, call.srcPos)
else if inlinedMethod == defn.Compiletime_envGetOrNull then
extractConstantValue(arg) match
case Some(key: String) =>
ctx.compileTimeEnvMap(key) match {
case Some(v) => return Literal(Constant(v))
case None => return Literal(Constant(null))
}
case _ =>
reportErrorConstantValueRequired(arg)
return Literal(Constant(null))
case _ =>

// Special handling of `constValue[T]` and `constValueOpt[T]`
Expand Down
1 change: 1 addition & 0 deletions compiler/test/dotty/tools/dotc/CompilationTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ class CompilationTests {
compileFile("tests/run-custom-args/fors.scala", defaultOptions.and("-source", "future")),
compileFile("tests/run-custom-args/no-useless-forwarders.scala", defaultOptions and "-Xmixin-force-forwarders:false"),
compileFile("tests/run-custom-args/defaults-serizaliable-no-forwarders.scala", defaultOptions and "-Xmixin-force-forwarders:false"),
compileFilesInDir("tests/run-custom-args/compileTimeEnv", defaultOptions.and("-Ea", "-Eb=1", "-Ec.b.a=x.y.z=1", "-EmyLogger.level=INFO", "-Xfatal-warnings")),
compileFilesInDir("tests/run-custom-args/erased", defaultOptions.and("-language:experimental.erasedDefinitions")),
compileFilesInDir("tests/run-deep-subtype", allowDeepSubtypes),
compileFilesInDir("tests/run", defaultOptions.and("-Ysafe-init"))
Expand Down
20 changes: 19 additions & 1 deletion library/src/scala/compiletime/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,25 @@ inline def summonAll[T <: Tuple]: T =
res.asInstanceOf[T]
end summonAll

@compileTimeOnly("Illegal reference to `scala.compiletime.envGetOrNull`")
transparent inline def envGetOrNull(inline key: String): String | Null =
Copy link
Contributor

Choose a reason for hiding this comment

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

This kind of logic should be in the reflection interface first. If it is, this method can be implemented in any external library.

// implemented in dotty.tools.dotc.typer.Inliner
error("Compiler bug: `envGetOrNull` was not evaluated by the compiler")

@compileTimeOnly("Illegal reference to `scala.compiletime.envGet`")
transparent inline def envGet(inline key: String): Option[String] =
inline envGetOrNull(key) match {
case null => None
case v => Some[v.type](v)
}

@compileTimeOnly("Illegal reference to `scala.compiletime.envGetOrElse`")
transparent inline def envGetOrElse(inline key: String, inline default: String): String =
inline envGetOrNull(key) match {
case null => default
case v => v
}

/** Assertion that an argument is by-name. Used for nullability checking. */
def byName[T](x: => T): T = x

Expand All @@ -168,4 +187,3 @@ def byName[T](x: => T): T = x
*/
extension [T](x: T)
transparent inline def asMatchable: x.type & Matchable = x.asInstanceOf[x.type & Matchable]

4 changes: 4 additions & 0 deletions tests/run-custom-args/compileTimeEnv/basic.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
a = []
b = [1]
c.b.a = [x.y.z=1]
wat is not defined
16 changes: 16 additions & 0 deletions tests/run-custom-args/compileTimeEnv/basic.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import scala.compiletime.*

object Test {

inline def logEnv(inline k: String): Unit =
inline envGet(k) match
case Some(v) => println(s"$k = [$v]")
case None => println(k + " is not defined")

def main(args: Array[String]): Unit = {
logEnv("a")
logEnv("b")
logEnv("c.b.a")
logEnv("wat")
}
}
2 changes: 2 additions & 0 deletions tests/run-custom-args/compileTimeEnv/logging.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
I'm a info msg
I'm a warn msg
38 changes: 38 additions & 0 deletions tests/run-custom-args/compileTimeEnv/logging.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import scala.compiletime.*

object Logging {

// Just use your imagination for now :)
private inline val Trace = 0
private inline val Debug = 1
private inline val Info = 2
private inline val Warn = 3

private transparent inline def chosenThreshold: Int =
inline envGet("myLogger.level") match
case Some("TRACE") => Trace
case Some("DEBUG") => Debug
case Some("INFO") => Info
case Some("WARN") => Warn
case Some(x) => error("Unsupported logging level: " + x)
case None => Trace

private inline def log(inline lvl: Int, inline msg: String): Unit =
inline if lvl >= chosenThreshold then println(msg) else ()

inline def trace(inline msg: String) = log(Trace, msg)
inline def debug(inline msg: String) = log(Debug, msg)
inline def info (inline msg: String) = log(Info , msg)
inline def warn (inline msg: String) = log(Warn , msg)
}

object Test {
import Logging.*

def main(args: Array[String]): Unit = {
trace("I'm a trace msg")
debug("I'm a debug msg")
info("I'm a info msg")
warn("I'm a warn msg")
}
}