From 348c8fac9f897f9661f84e32949b8a4e0c99e93a Mon Sep 17 00:00:00 2001 From: Eugene Burmako Date: Tue, 25 Dec 2012 02:37:31 +0100 Subject: 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. --- .../scala/reflect/macros/runtime/Context.scala | 1 + .../scala/reflect/macros/runtime/Synthetics.scala | 83 ++++++++++++++++ .../scala/tools/nsc/CompilationUnits.scala | 24 ++++- .../tools/nsc/ast/parser/SyntaxAnalyzer.scala | 12 ++- .../tools/nsc/typechecker/NamesDefaults.scala | 2 +- src/reflect/scala/reflect/api/StandardNames.scala | 5 + src/reflect/scala/reflect/internal/Mirrors.scala | 2 +- src/reflect/scala/reflect/internal/TreeGen.scala | 21 +++- src/reflect/scala/reflect/io/AbstractFile.scala | 3 + src/reflect/scala/reflect/io/NoAbstractFile.scala | 1 + .../scala/reflect/io/VirtualDirectory.scala | 1 + src/reflect/scala/reflect/io/VirtualFile.scala | 5 +- src/reflect/scala/reflect/macros/Context.scala | 3 +- src/reflect/scala/reflect/macros/Synthetics.scala | 106 +++++++++++++++++++++ src/reflect/scala/reflect/macros/TreeBuilder.scala | 16 +++- 15 files changed, 265 insertions(+), 20 deletions(-) create mode 100644 src/compiler/scala/reflect/macros/runtime/Synthetics.scala create mode 100644 src/reflect/scala/reflect/macros/Synthetics.scala (limited to 'src') diff --git a/src/compiler/scala/reflect/macros/runtime/Context.scala b/src/compiler/scala/reflect/macros/runtime/Context.scala index 8e8b0fcea1..76c684f6d7 100644 --- a/src/compiler/scala/reflect/macros/runtime/Context.scala +++ b/src/compiler/scala/reflect/macros/runtime/Context.scala @@ -14,6 +14,7 @@ abstract class Context extends scala.reflect.macros.Context with Parsers with Evals with ExprUtils + with Synthetics with Traces { val universe: Global diff --git a/src/compiler/scala/reflect/macros/runtime/Synthetics.scala b/src/compiler/scala/reflect/macros/runtime/Synthetics.scala new file mode 100644 index 0000000000..73f3ab8d20 --- /dev/null +++ b/src/compiler/scala/reflect/macros/runtime/Synthetics.scala @@ -0,0 +1,83 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + */ + +package scala.reflect.macros +package runtime + +import java.util.UUID._ +import scala.reflect.internal.Flags._ +import scala.reflect.internal.util.BatchSourceFile +import scala.reflect.io.VirtualFile + +trait Synthetics { + self: Context => + + import global._ + import mirror.wrapMissing + + // getClassIfDefined and getModuleIfDefined cannot be used here + // because they don't work for stuff declared in the empty package + // (as specified in SLS, code inside non-empty packages cannot see + // declarations from the empty package, so compiler internals + // default to ignoring contents of the empty package) + // to the contrast, staticModule and staticClass are designed + // to be a part of the reflection API and, therefore, they + // correctly resolve all names + private def topLevelSymbol(name: Name): Symbol = wrapMissing { + if (name.isTermName) mirror.staticModule(name.toString) + else mirror.staticClass(name.toString) + } + + def topLevelDef(name: Name): Tree = + enclosingRun.units.toList.map(_.body).flatMap { + // it's okay to check `stat.symbol` here, because currently macros expand strictly after namer + // which means that by the earliest time one can call this method all top-level definitions will have already been entered + case PackageDef(_, stats) => stats filter (stat => stat.symbol != NoSymbol && stat.symbol == topLevelSymbol(name)) + case _ => Nil // should never happen, but better be safe than sorry + }.headOption getOrElse EmptyTree + + def topLevelRef(name: Name): Tree = { + if (topLevelDef(name).nonEmpty) gen.mkUnattributedRef(name) + else EmptyTree + } + + // TODO: provide a way to specify a pretty name for debugging purposes + private def randomFileName() = ( + "macroSynthetic-" + randomUUID().toString.replace("-", "") + ".scala" + ) + + def introduceTopLevel[T: PackageSpec](packagePrototype: T, definition: universe.ImplDef): RefTree = + introduceTopLevel(packagePrototype, List(definition)).head + + def introduceTopLevel[T: PackageSpec](packagePrototype: T, definitions: universe.ImplDef*): List[RefTree] = + introduceTopLevel(packagePrototype, definitions.toList) + + private def introduceTopLevel[T: PackageSpec](packagePrototype: T, definitions: List[universe.ImplDef]): List[RefTree] = { + val code @ PackageDef(pid, _) = implicitly[PackageSpec[T]].mkPackageDef(packagePrototype, definitions) + val syntheticFileName = randomFileName() + // compatibility with SBT + // on the one hand, we need to specify some jfile here, otherwise sbt crashes with an NPE (SI-6870) + // on the other hand, we can't specify the obvious enclosingUnit, because then sbt somehow fails to run tests using type macros + // okay, now let's specify a guaranteedly non-existent file in an existing directory (so that we don't run into permission problems) + val relatedJfile = enclosingUnit.source.file.file + val fakeJfile = if (relatedJfile != null) new java.io.File(relatedJfile.getParent, syntheticFileName) else null + val virtualFile = new VirtualFile(syntheticFileName) { override def file = fakeJfile } + val sourceFile = new BatchSourceFile(virtualFile, code.toString) + val unit = new CompilationUnit(sourceFile) + unit.body = code + universe.currentRun.compileLate(unit) + definitions map (definition => Select(pid, definition.name)) + } + + protected def mkPackageDef(name: String, stats: List[Tree]) = gen.mkPackageDef(name, stats) + + protected def mkPackageDef(name: TermName, stats: List[Tree]) = gen.mkPackageDef(name.toString, stats) + + protected def mkPackageDef(tree: RefTree, stats: List[Tree]) = PackageDef(tree, stats) + + protected def mkPackageDef(sym: Symbol, stats: List[Tree]) = { + assert(sym hasFlag PACKAGE, s"expected a package or package class symbol, found: $sym") + gen.mkPackageDef(sym.fullName.toString, stats) + } +} diff --git a/src/compiler/scala/tools/nsc/CompilationUnits.scala b/src/compiler/scala/tools/nsc/CompilationUnits.scala index a2108b8ced..15d365ab8c 100644 --- a/src/compiler/scala/tools/nsc/CompilationUnits.scala +++ b/src/compiler/scala/tools/nsc/CompilationUnits.scala @@ -39,11 +39,31 @@ trait CompilationUnits { self: Global => /** Note: depends now contains toplevel classes. * To get their sourcefiles, you need to dereference with .sourcefile */ - val depends = mutable.HashSet[Symbol]() + private[this] val _depends = mutable.HashSet[Symbol]() + // SBT compatibility (SI-6875) + // + // imagine we have a file named A.scala, which defines a trait named Foo and a module named Main + // Main contains a call to a macro, which calls c.introduceTopLevel to define a mock for Foo + // c.introduceTopLevel creates a virtual file Virt35af32.scala, which contains a class named FooMock extending Foo, + // and macro expansion instantiates FooMock. the stage is now set. let's see what happens next. + // + // without this workaround in scalac or without being patched itself, sbt will think that + // * Virt35af32 depends on A (because it extends Foo from A) + // * A depends on Virt35af32 (because it contains a macro expansion referring to FooMock from Virt35af32) + // + // after compiling A.scala, SBT will notice that it has a new source file named Virt35af32. + // it will also think that this file hasn't yet been compiled and since A depends on it + // it will think that A needs to be recompiled. + // + // recompilation will lead to another macro expansion. that another macro expansion might choose to create a fresh mock, + // producing another virtual file, say, Virtee509a, which will again trick SBT into thinking that A needs a recompile, + // which will lead to another macro expansion, which will produce another virtual file and so on + def depends = if (exists && !source.file.isVirtual) _depends else mutable.HashSet[Symbol]() /** so we can relink */ - val defined = mutable.HashSet[Symbol]() + private[this] val _defined = mutable.HashSet[Symbol]() + def defined = if (exists && !source.file.isVirtual) _defined else mutable.HashSet[Symbol]() /** Synthetic definitions generated by namer, eliminated by typer. */ diff --git a/src/compiler/scala/tools/nsc/ast/parser/SyntaxAnalyzer.scala b/src/compiler/scala/tools/nsc/ast/parser/SyntaxAnalyzer.scala index 8a9ce8907e..f1bf590ebf 100644 --- a/src/compiler/scala/tools/nsc/ast/parser/SyntaxAnalyzer.scala +++ b/src/compiler/scala/tools/nsc/ast/parser/SyntaxAnalyzer.scala @@ -23,10 +23,14 @@ abstract class SyntaxAnalyzer extends SubComponent with Parsers with MarkupParse def apply(unit: global.CompilationUnit) { import global._ informProgress("parsing " + unit) - unit.body = - if (unit.isJava) new JavaUnitParser(unit).parse() - else if (reporter.incompleteHandled) new UnitParser(unit).parse() - else new UnitParser(unit).smartParse() + // if the body is already filled in, do nothing + // otherwise compileLate is going to overwrite bodies of synthetic source files + if (unit.body == EmptyTree) { + unit.body = + if (unit.isJava) new JavaUnitParser(unit).parse() + else if (reporter.incompleteHandled) new UnitParser(unit).parse() + else new UnitParser(unit).smartParse() + } if (settings.Yrangepos.value && !reporter.hasErrors) validatePositions(unit.body) diff --git a/src/compiler/scala/tools/nsc/typechecker/NamesDefaults.scala b/src/compiler/scala/tools/nsc/typechecker/NamesDefaults.scala index 14c8d85836..dfc1196f2e 100644 --- a/src/compiler/scala/tools/nsc/typechecker/NamesDefaults.scala +++ b/src/compiler/scala/tools/nsc/typechecker/NamesDefaults.scala @@ -394,7 +394,7 @@ trait NamesDefaults { self: Analyzer => // TODO #3649 can create spurious errors when companion object is gone (because it becomes unlinked from scope) if (defGetter == NoSymbol) None // prevent crash in erroneous trees, #3649 else { - var default1 = qual match { + var default1: Tree = qual match { case Some(q) => gen.mkAttributedSelect(q.duplicate, defGetter) case None => gen.mkAttributedRef(defGetter) diff --git a/src/reflect/scala/reflect/api/StandardNames.scala b/src/reflect/scala/reflect/api/StandardNames.scala index 4886e4f8f7..6c78f18716 100644 --- a/src/reflect/scala/reflect/api/StandardNames.scala +++ b/src/reflect/scala/reflect/api/StandardNames.scala @@ -84,6 +84,11 @@ trait StandardNames { */ val ROOTPKG: NameType + /** The term name ``. + * Represents the empty package. + */ + val EMPTY_PACKAGE_NAME: NameType + /** The string " " (a single whitespace). * `LOCAL_SUFFIX_STRING` is appended to the names of local identifiers, * when it's necessary to prevent a naming conflict. For example, underlying fields diff --git a/src/reflect/scala/reflect/internal/Mirrors.scala b/src/reflect/scala/reflect/internal/Mirrors.scala index 6e76a7afb3..d9f1d90b62 100644 --- a/src/reflect/scala/reflect/internal/Mirrors.scala +++ b/src/reflect/scala/reflect/internal/Mirrors.scala @@ -207,7 +207,7 @@ trait Mirrors extends api.Mirrors { erasureString(classTag[T].runtimeClass) } - @inline private def wrapMissing(body: => Symbol): Symbol = + @inline final def wrapMissing(body: => Symbol): Symbol = try body catch { case _: MissingRequirementError => NoSymbol } diff --git a/src/reflect/scala/reflect/internal/TreeGen.scala b/src/reflect/scala/reflect/internal/TreeGen.scala index 0954432c77..6a2006e56f 100644 --- a/src/reflect/scala/reflect/internal/TreeGen.scala +++ b/src/reflect/scala/reflect/internal/TreeGen.scala @@ -113,7 +113,7 @@ abstract class TreeGen extends macros.TreeBuilder { } /** Builds a reference to given symbol with given stable prefix. */ - def mkAttributedRef(pre: Type, sym: Symbol): Tree = { + def mkAttributedRef(pre: Type, sym: Symbol): RefTree = { val qual = mkAttributedQualifier(pre) qual match { case EmptyTree => mkAttributedIdent(sym) @@ -123,10 +123,17 @@ abstract class TreeGen extends macros.TreeBuilder { } /** Builds a reference to given symbol. */ - def mkAttributedRef(sym: Symbol): Tree = + def mkAttributedRef(sym: Symbol): RefTree = if (sym.owner.isClass) mkAttributedRef(sym.owner.thisType, sym) else mkAttributedIdent(sym) + def mkUnattributedRef(sym: Symbol): RefTree = mkUnattributedRef(sym.fullNameAsName('.')) + + def mkUnattributedRef(fullName: Name): RefTree = { + val hd :: tl = nme.segments(fullName.toString, assumeTerm = fullName.isTermName) + tl.foldLeft(Ident(hd): RefTree)(Select(_,_)) + } + /** Replaces tree type with a stable type if possible */ def stabilize(tree: Tree): Tree = stableTypeFor(tree) match { case Some(tp) => tree setType tp @@ -153,13 +160,13 @@ abstract class TreeGen extends macros.TreeBuilder { def mkAttributedStableRef(sym: Symbol): Tree = stabilize(mkAttributedRef(sym)) - def mkAttributedThis(sym: Symbol): Tree = + def mkAttributedThis(sym: Symbol): This = This(sym.name.toTypeName) setSymbol sym setType sym.thisType - def mkAttributedIdent(sym: Symbol): Tree = + def mkAttributedIdent(sym: Symbol): RefTree = Ident(sym.name) setSymbol sym setType sym.tpeHK - def mkAttributedSelect(qual: Tree, sym: Symbol): Tree = { + def mkAttributedSelect(qual: Tree, sym: Symbol): RefTree = { // Tests involving the repl fail without the .isEmptyPackage condition. if (qual.symbol != null && (qual.symbol.isEffectiveRoot || qual.symbol.isEmptyPackage)) mkAttributedIdent(sym) @@ -283,4 +290,8 @@ abstract class TreeGen extends macros.TreeBuilder { assert(ReflectRuntimeUniverse != NoSymbol) mkAttributedRef(ReflectRuntimeUniverse) setType singleType(ReflectRuntimeUniverse.owner.thisPrefix, ReflectRuntimeUniverse) } + + def mkPackageDef(packageName: String, stats: List[Tree]): PackageDef = { + PackageDef(mkUnattributedRef(newTermName(packageName)), stats) + } } diff --git a/src/reflect/scala/reflect/io/AbstractFile.scala b/src/reflect/scala/reflect/io/AbstractFile.scala index 1a8d1c4f5e..bd6c186825 100644 --- a/src/reflect/scala/reflect/io/AbstractFile.scala +++ b/src/reflect/scala/reflect/io/AbstractFile.scala @@ -124,6 +124,9 @@ abstract class AbstractFile extends Iterable[AbstractFile] { /** Is this abstract file a directory? */ def isDirectory: Boolean + /** Does this abstract file correspond to something on-disk? */ + def isVirtual: Boolean = false + /** Returns the time that this abstract file was last modified. */ def lastModified: Long diff --git a/src/reflect/scala/reflect/io/NoAbstractFile.scala b/src/reflect/scala/reflect/io/NoAbstractFile.scala index 8c88d3abf6..2c59fd8aae 100644 --- a/src/reflect/scala/reflect/io/NoAbstractFile.scala +++ b/src/reflect/scala/reflect/io/NoAbstractFile.scala @@ -22,6 +22,7 @@ object NoAbstractFile extends AbstractFile { def file: JFile = null def input: InputStream = null def isDirectory: Boolean = false + override def isVirtual: Boolean = true def iterator: Iterator[AbstractFile] = Iterator.empty def lastModified: Long = 0L def lookupName(name: String, directory: Boolean): AbstractFile = null diff --git a/src/reflect/scala/reflect/io/VirtualDirectory.scala b/src/reflect/scala/reflect/io/VirtualDirectory.scala index 94cb52e9b5..589076d693 100644 --- a/src/reflect/scala/reflect/io/VirtualDirectory.scala +++ b/src/reflect/scala/reflect/io/VirtualDirectory.scala @@ -26,6 +26,7 @@ extends AbstractFile { def container = maybeContainer.get def isDirectory = true + override def isVirtual = true val lastModified: Long = System.currentTimeMillis override def file = null diff --git a/src/reflect/scala/reflect/io/VirtualFile.scala b/src/reflect/scala/reflect/io/VirtualFile.scala index 09b977bd45..0dfa7d5473 100644 --- a/src/reflect/scala/reflect/io/VirtualFile.scala +++ b/src/reflect/scala/reflect/io/VirtualFile.scala @@ -37,7 +37,7 @@ class VirtualFile(val name: String, override val path: String) extends AbstractF def absolute = this /** Returns null. */ - final def file: JFile = null + def file: JFile = null override def sizeOption: Option[Int] = Some(content.size) @@ -57,6 +57,9 @@ class VirtualFile(val name: String, override val path: String) extends AbstractF /** Is this abstract file a directory? */ def isDirectory: Boolean = false + /** @inheritdoc */ + override def isVirtual: Boolean = true + /** Returns the time that this abstract file was last modified. */ private var _lastModified: Long = 0 def lastModified: Long = _lastModified diff --git a/src/reflect/scala/reflect/macros/Context.scala b/src/reflect/scala/reflect/macros/Context.scala index aa1c1db227..1adc6928da 100644 --- a/src/reflect/scala/reflect/macros/Context.scala +++ b/src/reflect/scala/reflect/macros/Context.scala @@ -36,7 +36,8 @@ trait Context extends Aliases with Typers with Parsers with Evals - with ExprUtils { + with ExprUtils + with Synthetics { /** The compile-time universe. */ val universe: Universe diff --git a/src/reflect/scala/reflect/macros/Synthetics.scala b/src/reflect/scala/reflect/macros/Synthetics.scala new file mode 100644 index 0000000000..14c6c930b3 --- /dev/null +++ b/src/reflect/scala/reflect/macros/Synthetics.scala @@ -0,0 +1,106 @@ +package scala.reflect +package macros + +/** + * EXPERIMENTAL + * + * A slice of [[scala.reflect.macros.Context the Scala macros context]] that + * exposes functions to introduce synthetic definitions. + * + * @define TOPLEVEL_TREE Top-level tree is a tree that represents a non-inner class or object in one of the currently compiled source files. + * Note that top-level isn't equivalent to [[scala.reflect.api.Symbols#SymbolApi.isStatic]], + * because static also embraces definitions nested in static objects + * + * @define INTRODUCE_TOP_LEVEL Allowed definitions include classes (represented by `ClassDef` trees), traits (represented + * by `ClassDef` trees having the `TRAIT` flag set in `mods`) and objects (represented by `ModuleDef` trees). + * + * The definitions are put into the package with a prototype provided in `packagePrototype`. + * Supported prototypes are (see [[PackageSpec]] for more details): + * * Strings and names representing a fully-qualified name of the package + * * Trees that can work as package ids + * * Package or package class symbols + * + * Typical value for a package prototype is a fully-qualified name in a string. + * For example, to generate a class available at `foo.bar.Test`, call this method as follows: + * + * introduceTopLevel("foo.bar", ClassDef(, TypeName("Test"), ,