Skip to content

Backport "Add expression compiler" to 3.3 LTS #291

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

Closed
wants to merge 36 commits into from
Closed
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
7056253
Add ExpressionCompiler in compiler module
adpi2 Feb 7, 2025
06b45fd
Add skeleton for debug tests
adpi2 Feb 11, 2025
d846017
Introduce debugMode in RunnerOrchestration
adpi2 Feb 11, 2025
e5cf656
Introduce debugMode in RunnerOrchestration
tgodzik Apr 24, 2025
10e9334
Add debugMain method
adpi2 Feb 12, 2025
6b37ea0
Add debugMain method
tgodzik Apr 24, 2025
1696498
Introduce Debugger
adpi2 Feb 12, 2025
8b5097b
Add DebugStepAssert and parsing
adpi2 Feb 12, 2025
75412e0
Add DebugStepAssert and parsing
tgodzik Apr 24, 2025
d523a47
Implement Debugger
adpi2 Feb 13, 2025
8c12021
Configure JDI with sbt-jdi-tools
adpi2 Feb 17, 2025
28bbe5f
Configure JDI with sbt-jdi-tools
tgodzik Apr 24, 2025
ea39507
Introduce and implement ExpressionEvaluator
adpi2 Feb 17, 2025
f9a2d4b
Add Eval step in debug check file
adpi2 Feb 18, 2025
5bf5147
Add multi-line error check
adpi2 Feb 18, 2025
9ead983
Hide progress bar when user is debugging the tests
adpi2 Feb 19, 2025
b60412b
Add eval-static-fields test
adpi2 Feb 19, 2025
615b6ea
Improve error reporting
adpi2 Feb 19, 2025
05e1b32
Fix re-using process for debugging
adpi2 Feb 20, 2025
7c91c89
Fix re-using process for debugging
tgodzik Apr 24, 2025
f68e690
Add eval-value-class test
adpi2 Feb 25, 2025
53e328e
Add more evaluation tests
adpi2 Feb 20, 2025
a75e166
Remove old Gen script for running debug tests
adpi2 Feb 27, 2025
d0f5e1f
Add documentation
adpi2 Feb 27, 2025
62a1231
Go to Add ExpressionCompiler
adpi2 Mar 7, 2025
375a247
Remove summaryReport.addCleanup
adpi2 Mar 7, 2025
a716b30
Remove summaryReport.addCleanup
tgodzik Apr 24, 2025
e81755a
Remove unused param in ExtractExpression.reflectEval
adpi2 Mar 7, 2025
f612ca0
Minor changes in ExtractExpression
adpi2 Mar 7, 2025
4c5a786
remove useless transform of inline val
adpi2 Mar 7, 2025
66f8208
Strenghten eval-java-protected-members test
adpi2 Mar 10, 2025
c574fd5
Add scaladoc on ExpressionCompiler
adpi2 Mar 10, 2025
d133f81
Add eval-explicit-nulls test
adpi2 Mar 10, 2025
4019273
Minor changes in InsertExpression
adpi2 Mar 10, 2025
ffbe49e
Minor changes in ResolveReflectEval
adpi2 Mar 10, 2025
44f16b6
Add scaladoc on expression compiler phases
adpi2 Mar 10, 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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ testlogs/

# Put local stuff here
local/
compiler/test/debug/Gen.jar

compiler/before-pickling.txt
compiler/after-pickling.txt
Expand Down
31 changes: 31 additions & 0 deletions compiler/src/dotty/tools/debug/ExpressionCompiler.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package dotty.tools.debug

import dotty.tools.dotc.Compiler
import dotty.tools.dotc.core.Contexts.Context
import dotty.tools.dotc.core.Phases.Phase
import dotty.tools.dotc.transform.ElimByName

/**
* The expression compiler powers the debug console in Metals and the IJ Scala plugin,
* enabling evaluation of arbitrary Scala expressions at runtime (even macros).
* It produces class files that can be loaded by the running Scala program,
* to compute the evaluation output.
*
* To do so, it extends the Compiler with 3 phases:
* - InsertExpression: parses and inserts the expression in the original source tree
* - ExtractExpression: extract the typed expression and places it in the new expression class
* - ResolveReflectEval: resolves local variables or inacessible members using reflection calls
*/
class ExpressionCompiler(config: ExpressionCompilerConfig) extends Compiler:

override protected def frontendPhases: List[List[Phase]] =
val parser :: others = super.frontendPhases: @unchecked
parser :: List(InsertExpression(config)) :: others

override protected def transformPhases: List[List[Phase]] =
val store = ExpressionStore()
// the ExtractExpression phase should be after ElimByName and ExtensionMethods, and before LambdaLift
val transformPhases = super.transformPhases
val index = transformPhases.indexWhere(_.exists(_.phaseName == ElimByName.name))
val (before, after) = transformPhases.splitAt(index + 1)
(before :+ List(ExtractExpression(config, store))) ++ (after :+ List(ResolveReflectEval(config, store)))
38 changes: 38 additions & 0 deletions compiler/src/dotty/tools/debug/ExpressionCompilerBridge.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package dotty.tools.debug

import java.nio.file.Path
import java.util.function.Consumer
import java.{util => ju}
import scala.jdk.CollectionConverters.*
import scala.util.control.NonFatal
import dotty.tools.dotc.reporting.StoreReporter
import dotty.tools.dotc.core.Contexts.Context
import dotty.tools.dotc.Driver

class ExpressionCompilerBridge:
def run(
outputDir: Path,
classPath: String,
options: Array[String],
sourceFile: Path,
config: ExpressionCompilerConfig
): Boolean =
val args = Array(
"-d",
outputDir.toString,
"-classpath",
classPath,
"-Yskip:pureStats"
// Debugging: Print the tree after phases of the debugger
// "-Vprint:insert-expression,resolve-reflect-eval",
) ++ options :+ sourceFile.toString
val driver = new Driver:
protected override def newCompiler(using Context): ExpressionCompiler = ExpressionCompiler(config)
val reporter = ExpressionReporter(error => config.errorReporter.accept(error))
try
driver.process(args, reporter)
!reporter.hasErrors
catch
case NonFatal(cause) =>
cause.printStackTrace()
throw cause
65 changes: 65 additions & 0 deletions compiler/src/dotty/tools/debug/ExpressionCompilerConfig.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package dotty.tools.debug

import dotty.tools.dotc.ast.tpd.*
import dotty.tools.dotc.core.Symbols.*
import dotty.tools.dotc.core.Types.*
import dotty.tools.dotc.core.Names.*
import dotty.tools.dotc.core.Flags.*
import dotty.tools.dotc.core.Contexts.*
import dotty.tools.dotc.core.SymUtils

import java.{util => ju}
import ju.function.Consumer

class ExpressionCompilerConfig private[debug] (
packageName: String,
outputClassName: String,
private[debug] val breakpointLine: Int,
private[debug] val expression: String,
private[debug] val localVariables: ju.Set[String],
private[debug] val errorReporter: Consumer[String],
private[debug] val testMode: Boolean
):
def this() = this(
packageName = "",
outputClassName = "",
breakpointLine = -1,
expression = "",
localVariables = ju.Collections.emptySet,
errorReporter = _ => (),
testMode = false,
)

def withPackageName(packageName: String): ExpressionCompilerConfig = copy(packageName = packageName)
def withOutputClassName(outputClassName: String): ExpressionCompilerConfig = copy(outputClassName = outputClassName)
def withBreakpointLine(breakpointLine: Int): ExpressionCompilerConfig = copy(breakpointLine = breakpointLine)
def withExpression(expression: String): ExpressionCompilerConfig = copy(expression = expression)
def withLocalVariables(localVariables: ju.Set[String]): ExpressionCompilerConfig = copy(localVariables = localVariables)
def withErrorReporter(errorReporter: Consumer[String]): ExpressionCompilerConfig = copy(errorReporter = errorReporter)

private[debug] val expressionTermName: TermName = termName(outputClassName.toLowerCase.toString)
private[debug] val expressionClassName: TypeName = typeName(outputClassName)

private[debug] def expressionClass(using Context): ClassSymbol =
if packageName.isEmpty then requiredClass(outputClassName)
else requiredClass(s"$packageName.$outputClassName")

private[debug] def evaluateMethod(using Context): Symbol =
expressionClass.info.decl(termName("evaluate")).symbol

private def copy(
packageName: String = packageName,
outputClassName: String = outputClassName,
breakpointLine: Int = breakpointLine,
expression: String = expression,
localVariables: ju.Set[String] = localVariables,
errorReporter: Consumer[String] = errorReporter,
) = new ExpressionCompilerConfig(
packageName,
outputClassName,
breakpointLine,
expression,
localVariables,
errorReporter,
testMode
)
17 changes: 17 additions & 0 deletions compiler/src/dotty/tools/debug/ExpressionReporter.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dotty.tools.debug

import dotty.tools.dotc.core.Contexts.*
import dotty.tools.dotc.reporting.AbstractReporter
import dotty.tools.dotc.reporting.Diagnostic

private class ExpressionReporter(reportError: String => Unit) extends AbstractReporter:
override def doReport(dia: Diagnostic)(using Context): Unit =
// Debugging: println(messageAndPos(dia))
dia match
case error: Diagnostic.Error =>
val newPos = error.pos.source.positionInUltimateSource(error.pos)
val errorWithNewPos = new Diagnostic.Error(error.msg, newPos)
reportError(stripColor(messageAndPos(errorWithNewPos)))
case _ =>
// TODO report the warnings in the expression
()
24 changes: 24 additions & 0 deletions compiler/src/dotty/tools/debug/ExpressionStore.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dotty.tools.debug

import dotty.tools.dotc.ast.tpd.*
import dotty.tools.dotc.core.Symbols.*
import dotty.tools.dotc.core.Types.*
import dotty.tools.dotc.core.Names.*
import dotty.tools.dotc.core.Flags.*
import dotty.tools.dotc.core.Contexts.*
import dotty.tools.dotc.core.SymUtils

private class ExpressionStore:
var symbol: TermSymbol | Null = null
// To resolve captured variables, we store:
// - All classes in the chain of owners of the expression
// - The first local method enclosing the expression
var classOwners: Seq[ClassSymbol] = Seq.empty
var capturingMethod: Option[TermSymbol] = None

def store(exprSym: Symbol)(using Context): Unit =
symbol = exprSym.asTerm
classOwners = exprSym.ownersIterator.collect { case cls: ClassSymbol => cls }.toSeq
capturingMethod = exprSym.ownersIterator
.find(sym => (sym.isClass || sym.is(Method)) && sym.enclosure.is(Method)) // the first local class or method
.collect { case sym if sym.is(Method) => sym.asTerm } // if it is a method
Loading