Skip to content

Add expression compiler #22597

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

Merged
merged 30 commits into from
Mar 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
26befcb
Add ExpressionCompiler in compiler module
adpi2 Feb 7, 2025
174a757
Add skeleton for debug tests
adpi2 Feb 11, 2025
afef97c
Introduce debugMode in RunnerOrchestration
adpi2 Feb 11, 2025
1378e1a
Add debugMain method
adpi2 Feb 12, 2025
62e0f40
Introduce Debugger
adpi2 Feb 12, 2025
32e538d
Add DebugStepAssert and parsing
adpi2 Feb 12, 2025
29f81e0
Implement Debugger
adpi2 Feb 13, 2025
ade2269
Configure JDI with sbt-jdi-tools
adpi2 Feb 17, 2025
43393ae
Introduce and implement ExpressionEvaluator
adpi2 Feb 17, 2025
50e03cc
Add Eval step in debug check file
adpi2 Feb 18, 2025
708607d
Add multi-line error check
adpi2 Feb 18, 2025
9354f4e
Hide progress bar when user is debugging the tests
adpi2 Feb 19, 2025
f14f73b
Add eval-static-fields test
adpi2 Feb 19, 2025
bfaec0f
Improve error reporting
adpi2 Feb 19, 2025
3e9f516
Fix re-using process for debugging
adpi2 Feb 20, 2025
9e36be6
Add eval-value-class test
adpi2 Feb 25, 2025
20c8280
Add more evaluation tests
adpi2 Feb 20, 2025
dffa9f4
Remove old Gen script for running debug tests
adpi2 Feb 27, 2025
176d8d1
Add documentation
adpi2 Feb 27, 2025
0855b09
Go to Add ExpressionCompiler
adpi2 Mar 7, 2025
ad715f7
Remove summaryReport.addCleanup
adpi2 Mar 7, 2025
c73fe36
Remove unused param in ExtractExpression.reflectEval
adpi2 Mar 7, 2025
5ca0b9a
Minor changes in ExtractExpression
adpi2 Mar 7, 2025
5c1a68e
remove useless transform of inline val
adpi2 Mar 7, 2025
9fcf8f1
Strenghten eval-java-protected-members test
adpi2 Mar 10, 2025
4672f2c
Add scaladoc on ExpressionCompiler
adpi2 Mar 10, 2025
6791207
Add eval-explicit-nulls test
adpi2 Mar 10, 2025
5a2c54a
Minor changes in InsertExpression
adpi2 Mar 10, 2025
bca9e31
Minor changes in ResolveReflectEval
adpi2 Mar 10, 2025
b75cd4d
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 @@ -62,7 +62,6 @@ testlogs/

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

/bin/.cp

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we report warnings? Maybe let's add a follow up issue later if this really necessary, but I would say it's not.

()
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
Loading