Skip to content

Commit 348c8fa

Browse files
committed
adds c.introduceTopLevel
The first in the family of mutators for the global symbol table, `introduceTopLevel` is capable of creating synthetic top-level classes and modules. The addition of nme.EMPTY_PACKAGE_NAME is necessary to let programmers insert definitions into the empty package. That's explicitly discouraged in the docs, but at times might come in handy. This patch introduce workarounds to avoid incompatibilities with SBT. First of all SBT doesn't like VirtualFiles having JFile set to null. Secondly SBT gets confused when someone depends on synthetic files added by c.introduceTopLevel. Strictly speaking these problems require changes to SBT, and that will be done later. However the main target of the patch is paradise/macros, which needs to be useful immediately, therefore we apply workarounds.
1 parent ed4f479 commit 348c8fa

34 files changed

+404
-27
lines changed

src/compiler/scala/reflect/macros/runtime/Context.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ abstract class Context extends scala.reflect.macros.Context
1414
with Parsers
1515
with Evals
1616
with ExprUtils
17+
with Synthetics
1718
with Traces {
1819

1920
val universe: Global
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/* NSC -- new Scala compiler
2+
* Copyright 2005-2013 LAMP/EPFL
3+
*/
4+
5+
package scala.reflect.macros
6+
package runtime
7+
8+
import java.util.UUID._
9+
import scala.reflect.internal.Flags._
10+
import scala.reflect.internal.util.BatchSourceFile
11+
import scala.reflect.io.VirtualFile
12+
13+
trait Synthetics {
14+
self: Context =>
15+
16+
import global._
17+
import mirror.wrapMissing
18+
19+
// getClassIfDefined and getModuleIfDefined cannot be used here
20+
// because they don't work for stuff declared in the empty package
21+
// (as specified in SLS, code inside non-empty packages cannot see
22+
// declarations from the empty package, so compiler internals
23+
// default to ignoring contents of the empty package)
24+
// to the contrast, staticModule and staticClass are designed
25+
// to be a part of the reflection API and, therefore, they
26+
// correctly resolve all names
27+
private def topLevelSymbol(name: Name): Symbol = wrapMissing {
28+
if (name.isTermName) mirror.staticModule(name.toString)
29+
else mirror.staticClass(name.toString)
30+
}
31+
32+
def topLevelDef(name: Name): Tree =
33+
enclosingRun.units.toList.map(_.body).flatMap {
34+
// it's okay to check `stat.symbol` here, because currently macros expand strictly after namer
35+
// which means that by the earliest time one can call this method all top-level definitions will have already been entered
36+
case PackageDef(_, stats) => stats filter (stat => stat.symbol != NoSymbol && stat.symbol == topLevelSymbol(name))
37+
case _ => Nil // should never happen, but better be safe than sorry
38+
}.headOption getOrElse EmptyTree
39+
40+
def topLevelRef(name: Name): Tree = {
41+
if (topLevelDef(name).nonEmpty) gen.mkUnattributedRef(name)
42+
else EmptyTree
43+
}
44+
45+
// TODO: provide a way to specify a pretty name for debugging purposes
46+
private def randomFileName() = (
47+
"macroSynthetic-" + randomUUID().toString.replace("-", "") + ".scala"
48+
)
49+
50+
def introduceTopLevel[T: PackageSpec](packagePrototype: T, definition: universe.ImplDef): RefTree =
51+
introduceTopLevel(packagePrototype, List(definition)).head
52+
53+
def introduceTopLevel[T: PackageSpec](packagePrototype: T, definitions: universe.ImplDef*): List[RefTree] =
54+
introduceTopLevel(packagePrototype, definitions.toList)
55+
56+
private def introduceTopLevel[T: PackageSpec](packagePrototype: T, definitions: List[universe.ImplDef]): List[RefTree] = {
57+
val code @ PackageDef(pid, _) = implicitly[PackageSpec[T]].mkPackageDef(packagePrototype, definitions)
58+
val syntheticFileName = randomFileName()
59+
// compatibility with SBT
60+
// on the one hand, we need to specify some jfile here, otherwise sbt crashes with an NPE (SI-6870)
61+
// on the other hand, we can't specify the obvious enclosingUnit, because then sbt somehow fails to run tests using type macros
62+
// okay, now let's specify a guaranteedly non-existent file in an existing directory (so that we don't run into permission problems)
63+
val relatedJfile = enclosingUnit.source.file.file
64+
val fakeJfile = if (relatedJfile != null) new java.io.File(relatedJfile.getParent, syntheticFileName) else null
65+
val virtualFile = new VirtualFile(syntheticFileName) { override def file = fakeJfile }
66+
val sourceFile = new BatchSourceFile(virtualFile, code.toString)
67+
val unit = new CompilationUnit(sourceFile)
68+
unit.body = code
69+
universe.currentRun.compileLate(unit)
70+
definitions map (definition => Select(pid, definition.name))
71+
}
72+
73+
protected def mkPackageDef(name: String, stats: List[Tree]) = gen.mkPackageDef(name, stats)
74+
75+
protected def mkPackageDef(name: TermName, stats: List[Tree]) = gen.mkPackageDef(name.toString, stats)
76+
77+
protected def mkPackageDef(tree: RefTree, stats: List[Tree]) = PackageDef(tree, stats)
78+
79+
protected def mkPackageDef(sym: Symbol, stats: List[Tree]) = {
80+
assert(sym hasFlag PACKAGE, s"expected a package or package class symbol, found: $sym")
81+
gen.mkPackageDef(sym.fullName.toString, stats)
82+
}
83+
}

src/compiler/scala/tools/nsc/CompilationUnits.scala

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,31 @@ trait CompilationUnits { self: Global =>
3939
/** Note: depends now contains toplevel classes.
4040
* To get their sourcefiles, you need to dereference with .sourcefile
4141
*/
42-
val depends = mutable.HashSet[Symbol]()
42+
private[this] val _depends = mutable.HashSet[Symbol]()
43+
// SBT compatibility (SI-6875)
44+
//
45+
// imagine we have a file named A.scala, which defines a trait named Foo and a module named Main
46+
// Main contains a call to a macro, which calls c.introduceTopLevel to define a mock for Foo
47+
// c.introduceTopLevel creates a virtual file Virt35af32.scala, which contains a class named FooMock extending Foo,
48+
// and macro expansion instantiates FooMock. the stage is now set. let's see what happens next.
49+
//
50+
// without this workaround in scalac or without being patched itself, sbt will think that
51+
// * Virt35af32 depends on A (because it extends Foo from A)
52+
// * A depends on Virt35af32 (because it contains a macro expansion referring to FooMock from Virt35af32)
53+
//
54+
// after compiling A.scala, SBT will notice that it has a new source file named Virt35af32.
55+
// it will also think that this file hasn't yet been compiled and since A depends on it
56+
// it will think that A needs to be recompiled.
57+
//
58+
// recompilation will lead to another macro expansion. that another macro expansion might choose to create a fresh mock,
59+
// producing another virtual file, say, Virtee509a, which will again trick SBT into thinking that A needs a recompile,
60+
// which will lead to another macro expansion, which will produce another virtual file and so on
61+
def depends = if (exists && !source.file.isVirtual) _depends else mutable.HashSet[Symbol]()
4362

4463
/** so we can relink
4564
*/
46-
val defined = mutable.HashSet[Symbol]()
65+
private[this] val _defined = mutable.HashSet[Symbol]()
66+
def defined = if (exists && !source.file.isVirtual) _defined else mutable.HashSet[Symbol]()
4767

4868
/** Synthetic definitions generated by namer, eliminated by typer.
4969
*/

src/compiler/scala/tools/nsc/ast/parser/SyntaxAnalyzer.scala

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ abstract class SyntaxAnalyzer extends SubComponent with Parsers with MarkupParse
2323
def apply(unit: global.CompilationUnit) {
2424
import global._
2525
informProgress("parsing " + unit)
26-
unit.body =
27-
if (unit.isJava) new JavaUnitParser(unit).parse()
28-
else if (reporter.incompleteHandled) new UnitParser(unit).parse()
29-
else new UnitParser(unit).smartParse()
26+
// if the body is already filled in, do nothing
27+
// otherwise compileLate is going to overwrite bodies of synthetic source files
28+
if (unit.body == EmptyTree) {
29+
unit.body =
30+
if (unit.isJava) new JavaUnitParser(unit).parse()
31+
else if (reporter.incompleteHandled) new UnitParser(unit).parse()
32+
else new UnitParser(unit).smartParse()
33+
}
3034

3135
if (settings.Yrangepos.value && !reporter.hasErrors)
3236
validatePositions(unit.body)

src/compiler/scala/tools/nsc/typechecker/NamesDefaults.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ trait NamesDefaults { self: Analyzer =>
394394
// TODO #3649 can create spurious errors when companion object is gone (because it becomes unlinked from scope)
395395
if (defGetter == NoSymbol) None // prevent crash in erroneous trees, #3649
396396
else {
397-
var default1 = qual match {
397+
var default1: Tree = qual match {
398398
case Some(q) => gen.mkAttributedSelect(q.duplicate, defGetter)
399399
case None => gen.mkAttributedRef(defGetter)
400400

src/reflect/scala/reflect/api/StandardNames.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ trait StandardNames {
8484
*/
8585
val ROOTPKG: NameType
8686

87+
/** The term name `<empty>`.
88+
* Represents the empty package.
89+
*/
90+
val EMPTY_PACKAGE_NAME: NameType
91+
8792
/** The string " " (a single whitespace).
8893
* `LOCAL_SUFFIX_STRING` is appended to the names of local identifiers,
8994
* when it's necessary to prevent a naming conflict. For example, underlying fields

src/reflect/scala/reflect/internal/Mirrors.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ trait Mirrors extends api.Mirrors {
207207
erasureString(classTag[T].runtimeClass)
208208
}
209209

210-
@inline private def wrapMissing(body: => Symbol): Symbol =
210+
@inline final def wrapMissing(body: => Symbol): Symbol =
211211
try body
212212
catch { case _: MissingRequirementError => NoSymbol }
213213

src/reflect/scala/reflect/internal/TreeGen.scala

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ abstract class TreeGen extends macros.TreeBuilder {
113113
}
114114

115115
/** Builds a reference to given symbol with given stable prefix. */
116-
def mkAttributedRef(pre: Type, sym: Symbol): Tree = {
116+
def mkAttributedRef(pre: Type, sym: Symbol): RefTree = {
117117
val qual = mkAttributedQualifier(pre)
118118
qual match {
119119
case EmptyTree => mkAttributedIdent(sym)
@@ -123,10 +123,17 @@ abstract class TreeGen extends macros.TreeBuilder {
123123
}
124124

125125
/** Builds a reference to given symbol. */
126-
def mkAttributedRef(sym: Symbol): Tree =
126+
def mkAttributedRef(sym: Symbol): RefTree =
127127
if (sym.owner.isClass) mkAttributedRef(sym.owner.thisType, sym)
128128
else mkAttributedIdent(sym)
129129

130+
def mkUnattributedRef(sym: Symbol): RefTree = mkUnattributedRef(sym.fullNameAsName('.'))
131+
132+
def mkUnattributedRef(fullName: Name): RefTree = {
133+
val hd :: tl = nme.segments(fullName.toString, assumeTerm = fullName.isTermName)
134+
tl.foldLeft(Ident(hd): RefTree)(Select(_,_))
135+
}
136+
130137
/** Replaces tree type with a stable type if possible */
131138
def stabilize(tree: Tree): Tree = stableTypeFor(tree) match {
132139
case Some(tp) => tree setType tp
@@ -153,13 +160,13 @@ abstract class TreeGen extends macros.TreeBuilder {
153160
def mkAttributedStableRef(sym: Symbol): Tree =
154161
stabilize(mkAttributedRef(sym))
155162

156-
def mkAttributedThis(sym: Symbol): Tree =
163+
def mkAttributedThis(sym: Symbol): This =
157164
This(sym.name.toTypeName) setSymbol sym setType sym.thisType
158165

159-
def mkAttributedIdent(sym: Symbol): Tree =
166+
def mkAttributedIdent(sym: Symbol): RefTree =
160167
Ident(sym.name) setSymbol sym setType sym.tpeHK
161168

162-
def mkAttributedSelect(qual: Tree, sym: Symbol): Tree = {
169+
def mkAttributedSelect(qual: Tree, sym: Symbol): RefTree = {
163170
// Tests involving the repl fail without the .isEmptyPackage condition.
164171
if (qual.symbol != null && (qual.symbol.isEffectiveRoot || qual.symbol.isEmptyPackage))
165172
mkAttributedIdent(sym)
@@ -283,4 +290,8 @@ abstract class TreeGen extends macros.TreeBuilder {
283290
assert(ReflectRuntimeUniverse != NoSymbol)
284291
mkAttributedRef(ReflectRuntimeUniverse) setType singleType(ReflectRuntimeUniverse.owner.thisPrefix, ReflectRuntimeUniverse)
285292
}
293+
294+
def mkPackageDef(packageName: String, stats: List[Tree]): PackageDef = {
295+
PackageDef(mkUnattributedRef(newTermName(packageName)), stats)
296+
}
286297
}

src/reflect/scala/reflect/io/AbstractFile.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ abstract class AbstractFile extends Iterable[AbstractFile] {
124124
/** Is this abstract file a directory? */
125125
def isDirectory: Boolean
126126

127+
/** Does this abstract file correspond to something on-disk? */
128+
def isVirtual: Boolean = false
129+
127130
/** Returns the time that this abstract file was last modified. */
128131
def lastModified: Long
129132

src/reflect/scala/reflect/io/NoAbstractFile.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ object NoAbstractFile extends AbstractFile {
2222
def file: JFile = null
2323
def input: InputStream = null
2424
def isDirectory: Boolean = false
25+
override def isVirtual: Boolean = true
2526
def iterator: Iterator[AbstractFile] = Iterator.empty
2627
def lastModified: Long = 0L
2728
def lookupName(name: String, directory: Boolean): AbstractFile = null

0 commit comments

Comments
 (0)