Skip to content

Improve var decoder #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 36 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e2f6e4e
Cache decoding failures
adpi2 Jan 14, 2025
c231b1e
override toString in jdi Symbols
adpi2 Jan 14, 2025
87bbab2
Update dependencies
adpi2 Jan 14, 2025
65231cc
reorganize tests
adpi2 Aug 7, 2024
7178a1c
split BinaryDecoder in 4: Class, Method, Field and Variable
adpi2 Aug 9, 2024
90d89c9
Try decode local variables of constructors
adpi2 Aug 9, 2024
76c58e1
Decode outer param and captures in constructors
adpi2 Aug 28, 2024
d1542c4
decode val in constructor
adpi2 Aug 29, 2024
3dc161d
Reduce ambiguity
adpi2 Aug 29, 2024
1cef2e1
Decode contextual params
adpi2 Sep 2, 2024
82b007f
Refactor VariableCollector
adpi2 Sep 3, 2024
ca25560
Find more captured variables
adpi2 Sep 3, 2024
1f1beb1
Reduce ambiguity of variables by comparing types
adpi2 Sep 3, 2024
b99d894
Decode more parameters
adpi2 Sep 4, 2024
5a83424
Fix isGenerated
adpi2 Jan 14, 2025
ddee0b0
Fix decoding captured lazy val
adpi2 Jan 14, 2025
f58459e
Decode anonymous parameter in Java method
adpi2 Jan 14, 2025
7e30bdb
Improve debug line guess in stats
adpi2 Jan 15, 2025
b93789d
Decode captured value class
adpi2 Jan 15, 2025
8600283
Decode setter param
adpi2 Jan 15, 2025
662d1f0
Update stats
adpi2 Jan 16, 2025
68ded30
Fix decoding contextual parameter in return type
adpi2 Jan 16, 2025
ae46c13
Introduce Scope to properly handle inlining
adpi2 Jan 16, 2025
efeb5aa
Formatting
adpi2 Jan 17, 2025
63e6759
Decode sbtLibPatches params
adpi2 Jan 17, 2025
938c7bc
Decode specialized params
adpi2 Jan 17, 2025
7f0759b
Refacto test classes
adpi2 Mar 4, 2025
c1e2f2d
Decode anon adapted params
adpi2 Mar 4, 2025
cf19912
Decode variables from inlined lambdas
adpi2 Mar 4, 2025
d96e694
Decode captured anon variable
adpi2 Mar 5, 2025
3938ea5
Fix decode proxy var from transparent inline
adpi2 Mar 5, 2025
3b70cfa
Use scoper to compute capture
adpi2 Mar 6, 2025
d818db9
Fix decoding capture from inline arg
adpi2 Mar 6, 2025
c7fddcf
Fix decode lazy param in inline init
adpi2 Mar 6, 2025
8af02ff
Fix decode captured proxy
adpi2 Mar 6, 2025
f962f94
Fix decode captured anon lazy val
adpi2 Mar 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version = "3.8.1"
version = "3.8.4"
project.git = true
align.preset = none
align.stripMargin = true
Expand Down
10 changes: 5 additions & 5 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import sbt._

object Dependencies {
val scala3Next = "3.4.2"
val asmVersion = "9.7"
val coursierVersion = "2.1.10"
val scala3Next = "3.6.2"
val asmVersion = "9.7.1"
val coursierVersion = "2.1.24"

val tastyQuery = "ch.epfl.scala" %% "tasty-query" % "1.3.0"
val tastyQuery = "ch.epfl.scala" %% "tasty-query" % "1.4.0"
val asm = "org.ow2.asm" % "asm" % asmVersion
val asmUtil = "org.ow2.asm" % "asm-util" % asmVersion

// test dependencies
val munit = "org.scalameta" %% "munit" % "1.0.0"
val munit = "org.scalameta" %% "munit" % "1.0.4"
val coursier = ("io.get-coursier" %% "coursier" % coursierVersion).cross(CrossVersion.for3Use2_13)
val coursierJvm = ("io.get-coursier" %% "coursier-jvm" % coursierVersion).cross(CrossVersion.for3Use2_13)
}
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.10.0
sbt.version=1.10.7
4 changes: 2 additions & 2 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12")
addSbtPlugin("org.scala-debugger" % "sbt-jdi-tools" % "1.1.1")
addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.9.2")
addSbtPlugin("com.github.sbt" % "sbt-jdi-tools" % "1.2.0")
172 changes: 172 additions & 0 deletions src/main/scala/ch/epfl/scala/decoder/BinaryClassDecoder.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package ch.epfl.scala.decoder

import ch.epfl.scala.decoder.internal.*
import tastyquery.Contexts.*
import tastyquery.Names.*
import tastyquery.Symbols.*

import scala.util.matching.Regex

trait BinaryClassDecoder(using Context, ThrowOrWarn):
self: BinaryDecoder =>

protected val scoper = Scoper()

def decode(cls: binary.BinaryClass): DecodedClass =
val javaParts = cls.name.split('.')
val packageNames = javaParts.dropRight(1).toList.map(SimpleName.apply)
val packageSym =
if packageNames.nonEmpty
then ctx.findSymbolFromRoot(packageNames).asInstanceOf[PackageSymbol]
else defn.EmptyPackage
val decodedClassName = NameTransformer.decode(javaParts.last)
val allSymbols = decodedClassName match
case Patterns.AnonClass(declaringClassName, remaining) =>
val WithLocalPart = "(.+)\\$(.+)\\$\\d+".r
val topLevelClassName = declaringClassName match
case WithLocalPart(topLevelClassName, _) => topLevelClassName.stripSuffix("$")
case topLevelClassName => topLevelClassName
reduceAmbiguityOnClasses(decodeLocalClasses(cls, packageSym, topLevelClassName, "$anon", remaining))
case Patterns.LocalClass(declaringClassName, localClassName, remaining) =>
decodeLocalClasses(cls, packageSym, declaringClassName, localClassName, remaining)
case _ => decodeClassFromPackage(packageSym, decodedClassName)

val candidates =
if cls.isObject then allSymbols.filter(_.isModuleClass)
else if cls.sourceLines.forall(_.isEmpty) && allSymbols.forall(_.isModuleClass) then
allSymbols.collect { case cls: DecodedClass.ClassDef => DecodedClass.SyntheticCompanionClass(cls.symbol) }
else allSymbols.filter(!_.isModuleClass)
candidates.singleOrThrow(cls)
end decode

private def reduceAmbiguityOnClasses(syms: Seq[DecodedClass]): Seq[DecodedClass] =
if syms.size > 1 then
val reduced = syms.filterNot(sym => syms.exists(enclose(sym, _)))
if reduced.size != 0 then reduced else syms
else syms

private def decodeLocalClasses(
javaClass: binary.BinaryClass,
packageSym: PackageSymbol,
declaringClassName: String,
localClassName: String,
remaining: Option[String]
): Seq[DecodedClass] =
val classOwners = decodeClassFromPackage(packageSym, declaringClassName).map(_.symbol)
remaining match
case None =>
val parents = (javaClass.superclass.toSet ++ javaClass.interfaces)
.map(decode)
.collect { case cls: DecodedClass.ClassDef => cls.symbol }
classOwners
.flatMap(cls => collectLocalClasses(cls, localClassName, javaClass.sourceLines))
.filter(matchParents(_, parents, javaClass.isInterface))
case Some(remaining) =>
val localClasses = classOwners
.flatMap(cls => collectLocalClasses(cls, localClassName, None))
.flatMap(_.classSymbol)
localClasses.flatMap(s => decodeClassRecursively(s, remaining))

private def decodeClassFromPackage(owner: PackageSymbol, decodedName: String): Seq[DecodedClass.ClassDef] =
val packageObject = "([^\\$]+\\$package)(\\$.*)?".r
val specializedClass = "([^\\$]+\\$mc.+\\$sp)(\\$.*)?".r
val standardClass = "([^\\$]+)(\\$.*)?".r
val topLevelName = decodedName match
case packageObject(name, _) => name
case specializedClass(name, _) => name
case standardClass(name, _) => name
val remaining = decodedName.stripPrefix(topLevelName).stripPrefix("$")
val typeNames = Seq(typeName(topLevelName), moduleClassName(topLevelName))
typeNames
.flatMap(owner.getDecl)
.collect { case sym: ClassSymbol => sym }
.flatMap { sym =>
if remaining.isEmpty then Seq(DecodedClass.ClassDef(sym))
else decodeClassRecursively(sym, remaining)
}

private def enclose(enclosing: DecodedClass, enclosed: DecodedClass): Boolean =
(enclosing, enclosed) match
case (enclosing: DecodedClass.InlinedClass, enclosed: DecodedClass.InlinedClass) =>
enclosing.callPos.enclose(enclosed.callPos) || (
!enclosed.callPos.enclose(enclosing.callPos) &&
enclose(enclosing.underlying, enclosed.underlying)
)
case (enclosing: DecodedClass.InlinedClass, enclosed) =>
enclosing.callPos.enclose(enclosed.pos)
case (enclosing, enclosed: DecodedClass.InlinedClass) =>
enclosing.pos.enclose(enclosed.callPos)
case (enclosing, enclosed) =>
enclosing.pos.enclose(enclosed.pos)

private def collectLocalClasses(
classSymbol: ClassSymbol,
name: String,
sourceLines: Option[binary.SourceLines]
): Seq[DecodedClass] =
val localClasses = collectLiftedTrees(classSymbol, sourceLines) {
case cls: LocalClass if cls.symbol.sourceName == name => cls
}
.map(cls => wrapIfInline(cls, DecodedClass.ClassDef(cls.symbol)))
val samAndPartialFunctions = collectLiftedTrees(classSymbol, sourceLines) { case lambda: LambdaTree => lambda }
.map { lambda =>
val (term, samClass) = lambda.symbol
wrapIfInline(lambda, DecodedClass.SAMOrPartialFunction(term, samClass, lambda.tpe.asInstanceOf))
}
localClasses ++ samAndPartialFunctions

private def matchParents(
decodedClass: DecodedClass,
expectedParents: Set[ClassSymbol],
isInterface: Boolean
): Boolean =
decodedClass match
case cls: DecodedClass.ClassDef =>
if cls.symbol.isEnum then expectedParents == cls.symbol.parentClasses.toSet + defn.ProductClass
else if isInterface then expectedParents == cls.symbol.parentClasses.filter(_.isTrait).toSet
else if cls.symbol.isAnonClass then cls.symbol.parentClasses.forall(expectedParents.contains)
else expectedParents == cls.symbol.parentClasses.toSet
case _: DecodedClass.SyntheticCompanionClass => false
case anonFun: DecodedClass.SAMOrPartialFunction =>
if anonFun.parentClass == Definitions.PartialFunctionClass then
expectedParents == Set(Definitions.AbstractPartialFunctionClass, Definitions.SerializableClass)
else expectedParents.contains(anonFun.parentClass)
case inlined: DecodedClass.InlinedClass => matchParents(inlined.underlying, expectedParents, isInterface)

private def decodeClassRecursively(owner: ClassSymbol, decodedName: String): Seq[DecodedClass.ClassDef] =
owner.declarations
.collect { case sym: ClassSymbol => sym }
.flatMap { sym =>
val Symbol = s"${Regex.quote(sym.sourceName)}\\$$?(.*)".r
decodedName match
case Symbol(remaining) =>
if remaining.isEmpty then Some(DecodedClass.ClassDef(sym))
else decodeClassRecursively(sym, remaining)
case _ => None
}

protected def collectLiftedTrees[S](owner: Symbol, sourceLines: Option[binary.SourceLines])(
matcher: PartialFunction[LiftedTree[?], LiftedTree[S]]
): Seq[LiftedTree[S]] =
val recursiveMatcher = new PartialFunction[LiftedTree[?], LiftedTree[S]]:
override def apply(tree: LiftedTree[?]): LiftedTree[S] = tree.asInstanceOf[LiftedTree[S]]
override def isDefinedAt(tree: LiftedTree[?]): Boolean = tree match
case InlinedFromArg(underlying, _, _) => isDefinedAt(underlying)
case InlinedFromDef(underlying, _) => isDefinedAt(underlying)
case _ => matcher.isDefinedAt(tree)
collectAllLiftedTrees(owner).collect(recursiveMatcher).filter(tree => sourceLines.forall(matchLines(tree, _)))

protected def collectAllLiftedTrees(owner: Symbol): Seq[LiftedTree[?]] =
LiftedTreeCollector.collect(owner)

private def wrapIfInline(liftedTree: LiftedTree[?], decodedClass: DecodedClass): DecodedClass =
liftedTree match
case InlinedFromDef(underlying, inlineCall) =>
DecodedClass.InlinedClass(wrapIfInline(underlying, decodedClass), inlineCall.callTree)
case _ => decodedClass

private def matchLines(liftedFun: LiftedTree[?], sourceLines: binary.SourceLines): Boolean =
// we use endsWith instead of == because of tasty-query#434
val positions =
liftedFun.scope(scoper).allPositions.filter(pos => pos.sourceFile.name.endsWith(sourceLines.sourceName))
sourceLines.tastyLines.forall(line => positions.exists(_.containsLine(line)))
Loading
Loading