diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 9f346f97e742..6041ddffd103 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -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.") diff --git a/compiler/src/dotty/tools/dotc/config/Settings.scala b/compiler/src/dotty/tools/dotc/config/Settings.scala index 7a0f18ab9e24..e8bf083952c6 100644 --- a/compiler/src/dotty/tools/dotc/config/Settings.scala +++ b/compiler/src/dotty/tools/dotc/config/Settings.scala @@ -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(_ != ':'))) diff --git a/compiler/src/dotty/tools/dotc/core/CompileTimeEnvMap.scala b/compiler/src/dotty/tools/dotc/core/CompileTimeEnvMap.scala new file mode 100644 index 000000000000..60a48eb66f79 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/core/CompileTimeEnvMap.scala @@ -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) + } +} diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index 60f675ff0b0d..2945db36d234 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -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 diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 4ae31b2e05e8..afc924c9b4c4 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -226,7 +226,7 @@ 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) @@ -234,6 +234,7 @@ class Definitions { @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") diff --git a/compiler/src/dotty/tools/dotc/typer/Inliner.scala b/compiler/src/dotty/tools/dotc/typer/Inliner.scala index d5893f44f631..da4cc9f1e5a9 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inliner.scala @@ -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]` diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index db05ed611a0e..a8d7a36969c8 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -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")) diff --git a/library/src/scala/compiletime/package.scala b/library/src/scala/compiletime/package.scala index 84ac4fde4e29..a53279451735 100644 --- a/library/src/scala/compiletime/package.scala +++ b/library/src/scala/compiletime/package.scala @@ -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 = + // 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 @@ -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] - diff --git a/tests/run-custom-args/compileTimeEnv/basic.check b/tests/run-custom-args/compileTimeEnv/basic.check new file mode 100644 index 000000000000..7c617526373a --- /dev/null +++ b/tests/run-custom-args/compileTimeEnv/basic.check @@ -0,0 +1,4 @@ +a = [] +b = [1] +c.b.a = [x.y.z=1] +wat is not defined diff --git a/tests/run-custom-args/compileTimeEnv/basic.scala b/tests/run-custom-args/compileTimeEnv/basic.scala new file mode 100644 index 000000000000..15cefefd7d64 --- /dev/null +++ b/tests/run-custom-args/compileTimeEnv/basic.scala @@ -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") + } +} diff --git a/tests/run-custom-args/compileTimeEnv/logging.check b/tests/run-custom-args/compileTimeEnv/logging.check new file mode 100644 index 000000000000..895742022505 --- /dev/null +++ b/tests/run-custom-args/compileTimeEnv/logging.check @@ -0,0 +1,2 @@ +I'm a info msg +I'm a warn msg diff --git a/tests/run-custom-args/compileTimeEnv/logging.scala b/tests/run-custom-args/compileTimeEnv/logging.scala new file mode 100644 index 000000000000..8c8d3c58333b --- /dev/null +++ b/tests/run-custom-args/compileTimeEnv/logging.scala @@ -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") + } +}