Skip to content

Commit 460bb2c

Browse files
committed
[DRAFT] Allow customisation of metaprogramming via scalac flags
1 parent 5780e5d commit 460bb2c

File tree

12 files changed

+148
-6
lines changed

12 files changed

+148
-6
lines changed

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ trait CommonScalaSettings { self: Settings.SettingGroup =>
4444
/** Other settings */
4545
val encoding: Setting[String] = StringSetting("-encoding", "encoding", "Specify character encoding used by source files.", Properties.sourceEncoding, aliases = List("--encoding"))
4646
val usejavacp: Setting[Boolean] = BooleanSetting("-usejavacp", "Utilize the java.class.path in classpath resolution.", aliases = List("--use-java-class-path"))
47+
val compileTimeEnv: Setting[List[String]] = MultiStringSetting("-E", "key[=value]", "Options to pass to the metaprogramming environment. e.g. -Ecom.example.app.mode=RELEASE")
4748

4849
/** Plugin-related setting */
4950
val plugin: Setting[List[String]] = MultiStringSetting ("-Xplugin", "paths", "Load a plugin from each classpath.")

compiler/src/dotty/tools/dotc/config/Settings.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,14 @@ object Settings {
166166

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

169+
// TODO Do properly
170+
if name == "-E" && prefix.isEmpty && arg.startsWith(name) then
171+
val s = arg.drop(name.length)
172+
if s.isEmpty || s.startsWith("=") then
173+
return state // Upstream will report as a bad option
174+
else
175+
return update(s :: Nil, args)
176+
169177
if (prefix != "" && arg.startsWith(prefix))
170178
doSet(arg drop prefix.length)
171179
else if (prefix == "" && matches(arg.takeWhile(_ != ':')))
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package dotty.tools.dotc.core
2+
3+
import dotty.tools.dotc.config.ScalaSettings
4+
import dotty.tools.dotc.config.Settings.Setting.value
5+
import Contexts._
6+
7+
// TODO doc
8+
final case class CompileTimeEnvMap(env: Map[String, String]) {
9+
def apply(key: String): Option[String] =
10+
env.get(key)
11+
}
12+
13+
object CompileTimeEnvMap {
14+
15+
def fromSettings(using Context): CompileTimeEnvMap = {
16+
var m = Map.empty[String, String]
17+
18+
for (s <- ctx.settings.compileTimeEnv.value)
19+
val i = s.indexOf('=')
20+
if i > 0 then
21+
// -Ekey=value
22+
val key = s.take(i)
23+
val value = s.drop(i + 1)
24+
m = m.updated(key, value)
25+
else if i < 0 then
26+
// -Ekey
27+
val key = s
28+
if !m.contains(key) then m = m.updated(key, "")
29+
30+
new CompileTimeEnvMap(m)
31+
}
32+
}

compiler/src/dotty/tools/dotc/core/Contexts.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,9 @@ object Contexts {
491491
/** Is the explicit nulls option set? */
492492
def explicitNulls: Boolean = base.settings.YexplicitNulls.value
493493

494+
lazy val compileTimeEnvMap: CompileTimeEnvMap =
495+
CompileTimeEnvMap.fromSettings
496+
494497
/** Initialize all context fields, except typerState, which has to be set separately
495498
* @param outer The outer context
496499
* @param origin The context from which fields are copied

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,14 +226,15 @@ class Definitions {
226226
@tu lazy val ScalaXmlPackageClass: Symbol = getPackageClassIfDefined("scala.xml")
227227

228228
@tu lazy val CompiletimePackageClass: Symbol = requiredPackage("scala.compiletime").moduleClass
229-
@tu lazy val Compiletime_codeOf: Symbol = CompiletimePackageClass.requiredMethod("codeOf")
229+
@tu lazy val Compiletime_codeOf : Symbol = CompiletimePackageClass.requiredMethod("codeOf")
230230
@tu lazy val Compiletime_erasedValue : Symbol = CompiletimePackageClass.requiredMethod("erasedValue")
231231
@tu lazy val Compiletime_uninitialized: Symbol = CompiletimePackageClass.requiredMethod("uninitialized")
232232
@tu lazy val Compiletime_error : Symbol = CompiletimePackageClass.requiredMethod(nme.error)
233233
@tu lazy val Compiletime_requireConst : Symbol = CompiletimePackageClass.requiredMethod("requireConst")
234234
@tu lazy val Compiletime_constValue : Symbol = CompiletimePackageClass.requiredMethod("constValue")
235235
@tu lazy val Compiletime_constValueOpt: Symbol = CompiletimePackageClass.requiredMethod("constValueOpt")
236236
@tu lazy val Compiletime_summonFrom : Symbol = CompiletimePackageClass.requiredMethod("summonFrom")
237+
@tu lazy val Compiletime_envGetOrNull : Symbol = CompiletimePackageClass.requiredMethod("envGetOrNull")
237238
@tu lazy val CompiletimeTestingPackage: Symbol = requiredPackage("scala.compiletime.testing")
238239
@tu lazy val CompiletimeTesting_typeChecks: Symbol = CompiletimeTestingPackage.requiredMethod("typeChecks")
239240
@tu lazy val CompiletimeTesting_typeCheckErrors: Symbol = CompiletimeTestingPackage.requiredMethod("typeCheckErrors")

compiler/src/dotty/tools/dotc/typer/Inliner.scala

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -665,19 +665,37 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) {
665665
case _ => EmptyTree
666666
}
667667

668+
private def extractConstantValue(t: Tree): Option[Any] =
669+
t match
670+
case ConstantValue(v) => Some(v)
671+
case Inlined(_, Nil, Typed(ConstantValue(v), _)) => Some(v)
672+
case _ => None
673+
674+
private def reportErrorConstantValueRequired(arg: Tree): Unit =
675+
report.error(em"expected a constant value but found: $arg", arg.srcPos)
676+
668677
/** The Inlined node representing the inlined call */
669678
def inlined(sourcePos: SrcPos): Tree = {
670679

671-
// Special handling of `requireConst` and `codeOf`
680+
// Special handling of `requireConst`, `codeOf`, `compileTimeEnv`
672681
callValueArgss match
673682
case (arg :: Nil) :: Nil =>
674683
if inlinedMethod == defn.Compiletime_requireConst then
675-
arg match
676-
case ConstantValue(_) | Inlined(_, Nil, Typed(ConstantValue(_), _)) => // ok
677-
case _ => report.error(em"expected a constant value but found: $arg", arg.srcPos)
684+
if extractConstantValue(arg).isEmpty then
685+
reportErrorConstantValueRequired(arg)
678686
return Literal(Constant(())).withSpan(sourcePos.span)
679687
else if inlinedMethod == defn.Compiletime_codeOf then
680688
return Intrinsics.codeOf(arg, call.srcPos)
689+
else if inlinedMethod == defn.Compiletime_envGetOrNull then
690+
extractConstantValue(arg) match
691+
case Some(key: String) =>
692+
ctx.compileTimeEnvMap(key) match {
693+
case Some(v) => return Literal(Constant(v))
694+
case None => return Literal(Constant(null))
695+
}
696+
case _ =>
697+
reportErrorConstantValueRequired(arg)
698+
return Literal(Constant(null))
681699
case _ =>
682700

683701
// Special handling of `constValue[T]` and `constValueOpt[T]`

compiler/test/dotty/tools/dotc/CompilationTests.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ class CompilationTests {
190190
compileFile("tests/run-custom-args/fors.scala", defaultOptions.and("-source", "future")),
191191
compileFile("tests/run-custom-args/no-useless-forwarders.scala", defaultOptions and "-Xmixin-force-forwarders:false"),
192192
compileFile("tests/run-custom-args/defaults-serizaliable-no-forwarders.scala", defaultOptions and "-Xmixin-force-forwarders:false"),
193+
compileFilesInDir("tests/run-custom-args/compileTimeEnv", defaultOptions.and("-Ea", "-Eb=1", "-Ec.b.a=x.y.z=1", "-EmyLogger.level=INFO", "-Xfatal-warnings")),
193194
compileFilesInDir("tests/run-custom-args/erased", defaultOptions.and("-language:experimental.erasedDefinitions")),
194195
compileFilesInDir("tests/run-deep-subtype", allowDeepSubtypes),
195196
compileFilesInDir("tests/run", defaultOptions.and("-Ysafe-init"))

library/src/scala/compiletime/package.scala

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,25 @@ inline def summonAll[T <: Tuple]: T =
156156
res.asInstanceOf[T]
157157
end summonAll
158158

159+
@compileTimeOnly("Illegal reference to `scala.compiletime.envGetOrNull`")
160+
transparent inline def envGetOrNull(inline key: String): String | Null =
161+
// implemented in dotty.tools.dotc.typer.Inliner
162+
error("Compiler bug: `envGetOrNull` was not evaluated by the compiler")
163+
164+
@compileTimeOnly("Illegal reference to `scala.compiletime.envGet`")
165+
transparent inline def envGet(inline key: String): Option[String] =
166+
inline envGetOrNull(key) match {
167+
case null => None
168+
case v => Some[v.type](v)
169+
}
170+
171+
@compileTimeOnly("Illegal reference to `scala.compiletime.envGetOrElse`")
172+
transparent inline def envGetOrElse(inline key: String, inline default: String): String =
173+
inline envGetOrNull(key) match {
174+
case null => default
175+
case v => v
176+
}
177+
159178
/** Assertion that an argument is by-name. Used for nullability checking. */
160179
def byName[T](x: => T): T = x
161180

@@ -168,4 +187,3 @@ def byName[T](x: => T): T = x
168187
*/
169188
extension [T](x: T)
170189
transparent inline def asMatchable: x.type & Matchable = x.asInstanceOf[x.type & Matchable]
171-
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
a = []
2+
b = [1]
3+
c.b.a = [x.y.z=1]
4+
wat is not defined
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import scala.compiletime.*
2+
3+
object Test {
4+
5+
inline def logEnv(inline k: String): Unit =
6+
inline envGet(k) match
7+
case Some(v) => println(s"$k = [$v]")
8+
case None => println(k + " is not defined")
9+
10+
def main(args: Array[String]): Unit = {
11+
logEnv("a")
12+
logEnv("b")
13+
logEnv("c.b.a")
14+
logEnv("wat")
15+
}
16+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
I'm a info msg
2+
I'm a warn msg
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import scala.compiletime.*
2+
3+
object Logging {
4+
5+
// Just use your imagination for now :)
6+
private inline val Trace = 0
7+
private inline val Debug = 1
8+
private inline val Info = 2
9+
private inline val Warn = 3
10+
11+
private transparent inline def chosenThreshold: Int =
12+
inline envGet("myLogger.level") match
13+
case Some("TRACE") => Trace
14+
case Some("DEBUG") => Debug
15+
case Some("INFO") => Info
16+
case Some("WARN") => Warn
17+
case Some(x) => error("Unsupported logging level: " + x)
18+
case None => Trace
19+
20+
private inline def log(inline lvl: Int, inline msg: String): Unit =
21+
inline if lvl >= chosenThreshold then println(msg) else ()
22+
23+
inline def trace(inline msg: String) = log(Trace, msg)
24+
inline def debug(inline msg: String) = log(Debug, msg)
25+
inline def info (inline msg: String) = log(Info , msg)
26+
inline def warn (inline msg: String) = log(Warn , msg)
27+
}
28+
29+
object Test {
30+
import Logging.*
31+
32+
def main(args: Array[String]): Unit = {
33+
trace("I'm a trace msg")
34+
debug("I'm a debug msg")
35+
info("I'm a info msg")
36+
warn("I'm a warn msg")
37+
}
38+
}

0 commit comments

Comments
 (0)