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

Add expression compiler #22597

merged 30 commits into from
Mar 10, 2025

Conversation

adpi2
Copy link
Member

@adpi2 adpi2 commented Feb 13, 2025

This PR migrates the expression compiler from scalacenter/scala-debug-adapter to the scala3 repository. This change was agreed upon a few months ago during a core meeting to simplify the release process. By doing so, we eliminate the need to release parts of scalacenter/scala-debug-adapter during each Scala 3 release.

The ExpressionCompiler and ExpressionCompilerBridge are now parts of the main compiler module. This integration streamlines usage for external tools like sbt, Metals, Bloop, IJ Scala plugin. They can now rely on the compiler JAR directly without needing to download additional artifacts.

The expression compiler

The expression compiler extends the main compiler with 3 phases:

InsertExpression:

  • Just after parsing
  • Parses and inserts the expression in the original source tree.
  • Inserts the expression class, with a placeholder def evaluate: Any method.

ExtractExpression:

  • After Typer, the pickler phases, ExtensionMethods and ElimByName.
  • Extracts the expression before LambdaLift to prevent interference: the expression itself can change how lambdas are lifted. Notably it can contains lambda that needs to be lifted to the expression class.
  • Replaces references to local variables and inaccessible members with magic calls to reflectEval, annotated with ReflectEvalStrategy attachments.

ResolveReflectEval:

  • At the end of the transform phases
  • Transforms reflectEval calls into actual reflective calls based on ReflectEvalStrategy metadata.

Usage of the expression compiler

The expression compiler powers the debug console in Metals and the IJ Scala plugin, enabling evaluation of arbitrary Scala expressions at runtime (even macros). The expression compiler produce class files that can be loaded the running Scala program, to compute the evaluation output.

The ExpressionCompilerBridge can be invoked by reflection with the following parameters:

  • outputDir: Path: Directory where compiled classes are written
  • classPath: String: Classpath used during compilation
  • options: Array[String]: Compiler options
  • sourceFile: Path: Source file where the expression is evaluated
  • config: ExpressionCompilerConfig:
    • packageName: Package name of the current evaluation frame
    • outputClassName: String: Name of the main generated class, to be loaded by the debuggee
    • breakpointLine: Int: Line number where the expression is evaluated
    • expression: String: Expression to compile
    • localVariables: Set[String]: Set of visible local variables (used for checking evaluation of captured variables)
    • errorReporter: Consumer[String]: Callback for reporting errors

The ExpressionCompilerConfig is designed as a factory class to allow binary-compatible evolution across versions, enabling cross-compatibility between different debugger and compiler versions.

The debug tests

A significant part of this PR is the migration of ScalaEvaluationTests, notably the integration with the vulpix test infrastructure:

  • In dotty.tools.vulpix.RunnerOrchestration:
    • added the debugMode to start runners with JVM debugging options
    • added the debugMain method to connect a debugger, start a main method and execute the debug steps
  • Added dotty.tools.debug.Debugger: a lightweight implementation of a JVM debugger
  • Added dotty.tools.debug.ExpressionEvaluator: to compile and evaluate expressions inside dotty.tools.debug.Debugger
  • Added dotty.tools.debug.DebugStepAssert: to describe a debug scenario as a series of steps (break, step, next and eval)
  • Added dotty.tools.debug.DebugTests test class:
    • it reads all Scala files and folders from tests/debug
    • for each Scala file, it parses its associated .check file as a series of DebugStepAssert
    • for each Scala file, it compiles it, runs it with debugMain, and executes the debug steps

I migrated all relevant tests from ScalaEvaluationTests to tests/debug folder. Thanks to Vulpix parallel execution, the 47 debug tests now complete in approximately 30-40 seconds.

@adpi2 adpi2 force-pushed the add-expression-compiler branch from 37985f6 to 839713b Compare February 13, 2025 15:42
@tgodzik
Copy link
Contributor

tgodzik commented Feb 17, 2025

Looks like the tests are not compiling due to usage of newer JDK API?

@adpi2
Copy link
Member Author

adpi2 commented Feb 17, 2025

Looks like the tests are not compiling due to usage of newer JDK API?

I need to configure sbt-jdi-tools to enable JDI on JDK 8.

@adpi2 adpi2 force-pushed the add-expression-compiler branch 3 times, most recently from 55e7262 to df7f853 Compare February 18, 2025 14:34
@adpi2 adpi2 force-pushed the add-expression-compiler branch 6 times, most recently from d60954c to b207244 Compare February 27, 2025 14:53
@adpi2 adpi2 marked this pull request as ready for review March 3, 2025 12:46
@adpi2 adpi2 requested review from sjrd and tgodzik March 3, 2025 12:46
@adpi2 adpi2 assigned tgodzik and sjrd Mar 4, 2025
Copy link
Member

@sjrd sjrd left a comment

Choose a reason for hiding this comment

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

Review of the tests and testing infrastructure so far (everything except the actual compiler/src/ things).

Copy link
Member

@sjrd sjrd left a comment

Choose a reason for hiding this comment

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

And here's the rest.

@adpi2 adpi2 force-pushed the add-expression-compiler branch 2 times, most recently from 11e4259 to 587896a Compare March 7, 2025 15:03
@adpi2 adpi2 force-pushed the add-expression-compiler branch from 587896a to 9fcf8f1 Compare March 10, 2025 08:16
@adpi2 adpi2 requested a review from sjrd March 10, 2025 10:24
@adpi2 adpi2 enabled auto-merge March 10, 2025 13:02
@adpi2 adpi2 merged commit 8a99f9f into scala:main Mar 10, 2025
28 checks passed
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.

override def phaseName: String = InsertExpression.name
override def isCheckable: Boolean = false

// TODO move reflection methods (callMethod, getField, etc) to scala3-library
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's maybe create an issue?

@WojciechMazur WojciechMazur added this to the 3.7.0 milestone Mar 11, 2025
@tgodzik tgodzik added the release-notes Should be mentioned in the release notes label Mar 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release-notes Should be mentioned in the release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants