Skip to content

Commit

Permalink
Merge pull request #433 from sjrd/thread-safe
Browse files Browse the repository at this point in the history
Make the entire API thread-safe.
  • Loading branch information
bishabosha authored Jan 13, 2024
2 parents 0a89f00 + f50b678 commit ce0fdb4
Show file tree
Hide file tree
Showing 17 changed files with 504 additions and 354 deletions.
5 changes: 5 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,13 @@ lazy val tastyQuery =
import com.typesafe.tools.mima.core.*
Seq(
// private[tastyquery], not an issue
ProblemFilters.exclude[DirectMissingMethodProblem]("tastyquery.Contexts#Context.classloader"),
ProblemFilters.exclude[MissingClassProblem]("tastyquery.Utils"),
ProblemFilters.exclude[MissingClassProblem]("tastyquery.Utils$"),
// private, not an issue
ProblemFilters.exclude[MissingClassProblem]("tastyquery.Types$TermRef$Resolved"),
ProblemFilters.exclude[MissingClassProblem]("tastyquery.Types$TypeRef$Resolved"),

// Everything in tastyquery.reader is private[tastyquery] at most
ProblemFilters.exclude[Problem]("tastyquery.reader.*"),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ object ClasspathLoaders:
* to create a [[Contexts.Context]]. The latter gives semantic access to all
* the definitions on the classpath.
*
* The entries of the resulting [[Classpaths.Classpath]] can be considered
* thread-safe, since the JavaScript environment is always single-threaded.
*
* @note the resulting [[Classpaths.ClasspathEntry ClasspathEntry]] entries of
* the returned [[Classpaths.Classpath]] correspond to the elements of `classpath`.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ object ClasspathLoaders {
* to create a [[Contexts.Context]]. The latter gives semantic access to all
* the definitions on the classpath.
*
* The entries of the resulting [[Classpaths.Classpath]] are all guaranteed
* to be thread-safe.
*
* @note the resulting [[Classpaths.ClasspathEntry ClasspathEntry]] entries of
* the returned [[Classpaths.Classpath]] correspond to the elements of `classpath`.
*/
Expand Down
28 changes: 10 additions & 18 deletions tasty-query/shared/src/main/scala/tastyquery/Annotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,16 @@ import tastyquery.Utils.*

object Annotations:
final class Annotation(val tree: TermTree):
private var mySymbol: ClassSymbol | Null = null
private var mySafeSymbol: Option[ClassSymbol] | Null = null
private var myArguments: List[TermTree] | Null = null
private val mySymbol: Memo[ClassSymbol] = uninitializedMemo
private val mySafeSymbol: Memo[Option[ClassSymbol]] = uninitializedMemo
private val myArguments: Memo[List[TermTree]] = uninitializedMemo

/** The annotation class symbol. */
def symbol(using Context): ClassSymbol =
memoized(
mySymbol,
{ computed =>
mySymbol = computed
mySafeSymbol = Some(computed)
}
) {
memoized2(mySymbol) {
computeAnnotSymbol(tree)
} { computed =>
initializeMemo(mySafeSymbol, Some(computed))
}
end symbol

Expand All @@ -35,14 +31,10 @@ object Annotations:
* If the class of this annotation cannot be successfully resolved, returns `false`.
*/
private[tastyquery] def safeHasSymbol(cls: ClassSymbol)(using Context): Boolean =
val safeSymbol = memoized(
mySafeSymbol,
{ computed =>
computed.foreach(mySymbol = _)
mySafeSymbol = computed
}
) {
val safeSymbol = memoized2(mySafeSymbol) {
computeSafeAnnotSymbol(tree)
} { computed =>
computed.foreach(sym => initializeMemo(mySymbol, sym))
}

safeSymbol.contains(cls)
Expand All @@ -64,7 +56,7 @@ object Annotations:
* `NamedArg`s are not visible with this method. They are replaced by
* their right-hand-side.
*/
def arguments: List[TermTree] = memoized(myArguments, myArguments = _) {
def arguments: List[TermTree] = memoized(myArguments) {
computeAnnotArguments(tree)
}

Expand Down
13 changes: 12 additions & 1 deletion tasty-query/shared/src/main/scala/tastyquery/Classpaths.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ object Classpaths:
* All the methods of `ClasspathEntry` and its components may throw
* `java.io.IOException`s to indicate I/O errors.
*
* A `ClasspathEntry` is encouraged to be thread-safe, along with all its
* components, but it is not a strong requirement. Implementations that are
* thread-safe should be documented as such. [[Contexts.Context]]s created
* only from thread-safe `ClasspathEntry`s are thread-safe themselves.
*
* Implementations of this class are encouraged to define a `toString()`
* method that helps identifying the entry for debugging purposes.
*/
Expand Down Expand Up @@ -99,16 +104,21 @@ object Classpaths:
def readClassFileBytes(): IArray[Byte]
end ClassData

/** In-memory representation of classpath entries. */
/** In-memory representation of classpath entries.
*
* In-memory classpath entries are thread-safe.
*/
object InMemory:
import Classpaths as generic

/** A thread-safe, immutable classpath entry. */
final class ClasspathEntry(debugString: String, val packages: List[PackageData]) extends generic.ClasspathEntry:
override def toString(): String = debugString

def listAllPackages(): List[generic.PackageData] = packages
end ClasspathEntry

/** A thread-safe, immutable package information within a classpath entry. */
final class PackageData(debugString: String, val dotSeparatedName: String, val classes: List[ClassData])
extends generic.PackageData:
private lazy val byBinaryName = classes.map(c => c.binaryName -> c).toMap
Expand All @@ -120,6 +130,7 @@ object Classpaths:
def getClassDataByBinaryName(binaryName: String): Option[generic.ClassData] = byBinaryName.get(binaryName)
end PackageData

/** A thread-safe, immutable class information within a classpath entry. */
final class ClassData(
debugString: String,
val binaryName: String,
Expand Down
19 changes: 17 additions & 2 deletions tasty-query/shared/src/main/scala/tastyquery/Contexts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,22 @@ object Contexts {

/** Factory methods for [[Context]]. */
object Context:
/** Creates a new [[Context]] for the given [[Classpaths.Classpath]]. */
/** Creates a new [[Context]] for the given [[Classpaths.Classpath]].
*
* If all the [[Classpaths.ClasspathEntry ClasspathEntries]] in the classpath
* are thread-safe, then the resulting [[Context]] is thread-safe.
*/
def initialize(classpath: Classpath): Context =
val classloader = Loader(classpath)
val ctx = new Context(classloader)
classloader.initPackages()(using ctx)

/* Exploit the portable releaseFence() call inside the `::` constructor,
* in order to publish all the mutations that were done during the
* above initialization to other threads.
*/
new ::(Nil, Nil)

ctx
end initialize
end Context
Expand All @@ -58,15 +69,19 @@ object Contexts {
* The same instance of [[Classpaths.Classpath]] can be reused to create
* several [[Context]]s, if necessary.
*/
final class Context private[Contexts] (private[tastyquery] val classloader: Loader) {
final class Context private[Contexts] (classloader: Loader) {
private given Context = this

private val sourceFiles = mutable.HashMap.empty[String, SourceFile]

private val (RootPackage @ _, EmptyPackage @ _) = PackageSymbol.createRoots()

private[tastyquery] def hasGenericTuples: Boolean = classloader.hasGenericTuples

val defn: Definitions = Definitions(this: @unchecked, RootPackage, EmptyPackage)

private[tastyquery] def internalClasspathForTestsOnly: Classpath = classloader.classpath

private[tastyquery] def getSourceFile(path: String): SourceFile =
sourceFiles.getOrElseUpdate(path, new SourceFile(path))

Expand Down
53 changes: 36 additions & 17 deletions tasty-query/shared/src/main/scala/tastyquery/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,19 @@ import tastyquery.Names.*
import tastyquery.Symbols.*
import tastyquery.Types.*

final class Definitions private[tastyquery] (ctx: Context, rootPackage: PackageSymbol, emptyPackage: PackageSymbol):
private given Context = ctx
final class Definitions private[tastyquery] (
ctxRestricted: Context,
rootPackage: PackageSymbol,
emptyPackage: PackageSymbol
):
/** Use the restricted context for an op.
*
* !!! ONLY use from the initialization code of `lazy val`s.
*
* Well ... `def FunctionNClass` also uses it, for compatibility reasons, but it's fine.
*/
private inline def withRestrictedContext[A](op: Context ?=> A): A =
op(using ctxRestricted)

// Core packages

Expand Down Expand Up @@ -284,15 +295,17 @@ final class Definitions private[tastyquery] (ctx: Context, rootPackage: PackageS
createSpecialMethod(cls, nme.m_synchronized, synchronizedTpe)
end createObjectMagicMethods

lazy val Object_eq: TermSymbol = ObjectClass.findNonOverloadedDecl(nme.m_eq)
lazy val Object_ne: TermSymbol = ObjectClass.findNonOverloadedDecl(nme.m_ne)
lazy val Object_synchronized: TermSymbol = ObjectClass.findNonOverloadedDecl(nme.m_synchronized)
lazy val Object_eq: TermSymbol = withRestrictedContext(ObjectClass.findNonOverloadedDecl(nme.m_eq))
lazy val Object_ne: TermSymbol = withRestrictedContext(ObjectClass.findNonOverloadedDecl(nme.m_ne))

lazy val Object_synchronized: TermSymbol =
withRestrictedContext(ObjectClass.findNonOverloadedDecl(nme.m_synchronized))

private[tastyquery] def createStringMagicMethods(cls: ClassSymbol): Unit =
createSpecialMethod(cls, nme.m_+, stringConcatMethodType, Final)
end createStringMagicMethods

lazy val String_+ : TermSymbol = StringClass.findNonOverloadedDecl(nme.m_+)
lazy val String_+ : TermSymbol = withRestrictedContext(StringClass.findNonOverloadedDecl(nme.m_+))

private[tastyquery] def createEnumMagicMethods(cls: ClassSymbol): Unit =
val ctorTpe = PolyType(List(typeName("E")), List(NothingAnyBounds), MethodType(Nil, Nil, UnitType))
Expand Down Expand Up @@ -412,8 +425,12 @@ final class Definitions private[tastyquery] (ctx: Context, rootPackage: PackageS
// Derived symbols, found on the classpath

extension (pkg: PackageSymbol)
private def requiredClass(name: String): ClassSymbol = pkg.getDecl(typeName(name)).get.asClass
private def optionalClass(name: String): Option[ClassSymbol] = pkg.getDecl(typeName(name)).map(_.asClass)
private def requiredClass(name: String): ClassSymbol =
withRestrictedContext(pkg.getDecl(typeName(name)).get.asClass)

private def optionalClass(name: String): Option[ClassSymbol] =
withRestrictedContext(pkg.getDecl(typeName(name)).map(_.asClass))
end extension

lazy val ObjectClass = javaLangPackage.requiredClass("Object")

Expand All @@ -423,7 +440,7 @@ final class Definitions private[tastyquery] (ctx: Context, rootPackage: PackageS
lazy val Function0Class = scalaPackage.requiredClass("Function0")

def FunctionNClass(n: Int): ClassSymbol =
scalaPackage.requiredClass(s"Function$n")
withRestrictedContext(scalaPackage.findDecl(typeName(s"Function$n")).asClass)

lazy val IntClass = scalaPackage.requiredClass("Int")
lazy val LongClass = scalaPackage.requiredClass("Long")
Expand Down Expand Up @@ -465,15 +482,15 @@ final class Definitions private[tastyquery] (ctx: Context, rootPackage: PackageS

private[tastyquery] lazy val PolyFunctionClass = scalaPackage.optionalClass("PolyFunction")

private[tastyquery] def isPolyFunctionSub(tpe: Type): Boolean =
private[tastyquery] def isPolyFunctionSub(tpe: Type)(using Context): Boolean =
PolyFunctionClass.exists(cls => tpe.baseType(cls).isDefined)

private[tastyquery] def isPolyFunctionSub(prefix: Prefix): Boolean = prefix match
private[tastyquery] def isPolyFunctionSub(prefix: Prefix)(using Context): Boolean = prefix match
case tpe: Type => isPolyFunctionSub(tpe)
case _ => false

private[tastyquery] object PolyFunctionType:
def unapply(tpe: TermRefinement): Option[MethodicType] =
def unapply(tpe: TermRefinement)(using Context): Option[MethodicType] =
PolyFunctionClass match
case None =>
None
Expand All @@ -488,7 +505,7 @@ final class Definitions private[tastyquery] (ctx: Context, rootPackage: PackageS
None
end unapply

private[tastyquery] def functionClassOf(mt: MethodicType): ClassSymbol = mt match
private[tastyquery] def functionClassOf(mt: MethodicType)(using Context): ClassSymbol = mt match
case mt: PolyType =>
mt.resultType match
case resultType: MethodicType =>
Expand All @@ -500,15 +517,17 @@ final class Definitions private[tastyquery] (ctx: Context, rootPackage: PackageS
end functionClassOf
end PolyFunctionType

lazy val hasGenericTuples = ctx.classloader.hasGenericTuples
lazy val hasGenericTuples = withRestrictedContext(ctx.hasGenericTuples)

lazy val uninitializedMethod: Option[TermSymbol] =
scalaCompiletimePackage.getDecl(moduleClassName("package$package")).flatMap { packageObjectClass =>
packageObjectClass.asClass.getDecl(termName("uninitialized"))
withRestrictedContext {
scalaCompiletimePackage.getDecl(moduleClassName("package$package")).flatMap { packageObjectClass =>
packageObjectClass.asClass.getDecl(termName("uninitialized"))
}
}
end uninitializedMethod

private[tastyquery] lazy val uninitializedMethodTermRef: TermRef =
TermRef(TermRef(defn.scalaCompiletimePackage.packageRef, termName("package$package")), termName("uninitialized"))
TermRef(TermRef(scalaCompiletimePackage.packageRef, termName("package$package")), termName("uninitialized"))

end Definitions
Loading

0 comments on commit ce0fdb4

Please sign in to comment.