Skip to content
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

update readme #556

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
99 changes: 85 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# scala-debug-adapter

[![build-badge][]][build]
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/ch.epfl.scala/sbt-debug-adapter/badge.svg)](https://maven-badges.herokuapp.com/maven-central/ch.epfl.scala/sbt-debug-adapter)

Expand All @@ -9,11 +10,12 @@ The scala-debug-adapter is a server-side implementation of the [Debug Adapter Pr
It is based on and extends the [microsoft/java-debug](https://github.com/microsoft/java-debug) implementation.

The project originated in the [Bloop](https://github.com/scalacenter/bloop) repository, it is now released independently so that it can be used in other build tools of the Scala ecosystem.
For instance, the [sbt-debug-adapter](#sbt-debug-adapter) is an sbt plugin that provides sbt with the debug adapter capability.
For instance, the [sbt-debug-adapter](#sbt-debug-adapter plugin) is an sbt plugin that provides sbt with the debug adapter capability.

## Usage

You can add the `scala-debug-adapter` as a dependency in your `build.sbt`:

```scala
// build.sbt
scalaVersion := "2.12.16",
Expand All @@ -28,7 +30,7 @@ While it is always the case with Java 9, you probably want to use the `sbt-jdi-t
addSbtPlugin("org.scala-debugger" % "sbt-jdi-tools" % "1.1.1")
```

You can start a debug server by providing your own intance of `Debuggee`, `DebugToolsResolver` and `Logger`.
You can start a debug server by providing your own instance of `Debuggee`, `DebugToolsResolver` and `Logger`.

### The `Debuggee`

Expand All @@ -37,6 +39,7 @@ The `Debuggee` is a trait that describes the program that we want to debug.
It contains the list of modules of the current project, the list of libraries, the list of unmanaged entries and the java runtime.

There are a few difference between a module, a library and an unmanaged entry:

- `Module`: We know its exact scala version, and its compiler options.
They will be used by the debugger to evaluate expressions in the module.
- `Library`: We only know its binary version.
Expand All @@ -51,11 +54,12 @@ The sbt version of `Debuggee` can be found [here](https://github.com/scalacenter

### The `DebugToolsResolver`

Depending on the Scala versions specified in the `Debuggee`, the debugger will need to resolve some additionnal tools:
Depending on the Scala versions specified in the `Debuggee`, the debugger will need to resolve some additional tools:

- The expression compiler: to compile the Scala expression that the user wants to evaluate
- The step filter: to filter the intermediate steps of execution that are generated by the compiler, among which the mixin-forwarders, the bridges, the getters and setters, the synthetized methods.
- The step filter (renamed to `unpickler`): to filter the intermediate steps of execution that are generated by the compiler, among which the mixin-forwarders, the bridges, the getters and setters, the synthetized methods.

The role of the `DebugToolsResolver` is to resovle the debug tools and their dependencies, and to load them in a class-loader.
The role of the `DebugToolsResolver` is to resolve the debug tools and their dependencies, and to load them in a class-loader.
The debugger takes care of reusing those class-loaders as often as possible: it should call the `DebugToolsResolver` only once by Scala version.

To implement the `DebugToolsResolver` you can use the Maven coordinates of the required tools in `ch.epfl.scala.debugadapter.BuildInfo`.
Expand All @@ -74,21 +78,21 @@ val address = new DebugServer.Address()
val tools = DebugTools(debuggee, resolver, logger)

// create and start the debug server
val server = DebugServer(debugge, tools, logger, address)
val server = DebugServer(debuggee, tools, logger, address)
server.start()

// return address.uri for the DAP client to connect
address.uri
```

# sbt-debug-adapter
## sbt-debug-adapter plugin

The `sbt-debug-adapter` is an sbt plugin compatible with sbt `1.4.0` or greater.
It provides the sbt server with the BSP `debugSession/start` endpoint to start a Scala DAP server.

The specification of the `debugSession/start` endpoint can be found in the [Bloop documentation](https://scalacenter.github.io/bloop/docs/debugging-reference).

## Usage
### Plugin usage

As a global plugin use `~/.sbt/1.0/plugins/plugins.sbt`, otherwise add to your local project (e.g. `project/plugins.sbt`):

Expand All @@ -105,19 +109,86 @@ To do so you can use the `sbt-jdi-tools` plugin in the meta project (it goes to
addSbtPlugin("org.scala-debugger" % "sbt-jdi-tools" % "1.1.1")
```

# Development
## Development
iusildra marked this conversation as resolved.
Show resolved Hide resolved

### expression-compiler

The [`expression-compiler`](https://github.com/scalacenter/scala-debug-adapter/tree/main/modules/expression-compiler) is a module used to compile expression from the debug console. It will insert the expression in the source file, compile it, and return the result of the expression.

To do so, it adds 2 new phases to the Scala compiler:
iusildra marked this conversation as resolved.
Show resolved Hide resolved

- `extract-expression`: extract the expression from the source file in a new class from where we can evaluate it. To keep the context (local variables), we import them in the new class via a `Map`
- `resolve-reflect-eval`: use reflection to resolve some symbols of the expression

## tests
Debugging the `expression-compiler` requires to enable some logs to show the trees generated by the compiler and the expression compiler. To do so, you need to uncomment the following lines:

- `println(messageAndPos(...))` in `ExpressionReporter.scala`
- For Scala 3.1+: [file](https://github.com/iusildra/scala-debug-adapter/blob/main/modules/expression-compiler/src/main/scala-3.1+/dotty/tools/dotc/ExpressionReporter.scala)
- For Scala 3.0: [file](https://github.com/iusildra/scala-debug-adapter/blob/main/modules/expression-compiler/src/main/scala-3.0/dotty/tools/dotc/ExpressionReporter.scala)
- `// "-Vprint:typer,extract-expression,resolve-reflect-eval"` in [`ExpressionCompilerBridge.scala`](https://github.com/iusildra/scala-debug-adapter/blob/main/modules/expression-compiler/src/main/scala-3/dotty/tools/dotc/ExpressionCompilerBridge.scala)

### Runtime evaluator

#### Idea

The runtime evaluator is a fully reflection-based evaluation mode designed for simple expressions (without implicits, generics, etc.) written in the debug console. It is less precise than the expression compiler (that can evaluate any kind of expression), but it is much faster (since it does not need to compile). Also, because it is reflection-based, we can access runtime types and private members, which is not possible with the expression compiler. For instance:

```scala
class A { private val x = 64 }
class B extends A { val y = 42 }

val a: A = new A
val b: A = new B

// a.x returns 64
// b.y returns 42
```

#### Constraints

Because it is less precise than the expression compiler, it might fail while evaluating an expression (and placing the program in an invalid state), or evaluate it incorrectly (bad overload resolution for instance). To overcome this, a validation step is performed before any evaluation and an order of priority has been defined to choose between the expression compiler and the runtime evaluator:

![evaluation mode priorities](./doc/evaluation-provider.svg)

#### Implementation

The evaluator is implemented in a 3-steps process:

- parse the expression with [scalameta](https://scalameta.org/) to get the tree representing the expression
- validation: traverse the tree and recursively check that the expression and its sub-expressions can be evaluated at runtime (type-checking, polymorphism overloads resolution, type resolution, etc.). Transform the AST from scalameta to a new AST containing all the information needed for evaluation (see [RuntimeTree.scala](https://github.com/scalacenter/scala-debug-adapter/blob/main/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/RuntimeTree.scala))
- evaluation: consume the new AST and evaluate the expression

The validation AST is separated in 2 main parts:

- `RuntimeEvaluableTree`: these nodes are valid expressions by themselves (e.g. a literal, a method call, a module, etc.)
- `RuntimeValidationTree`: theses nodes are not a valid expression by themselves (e.g. a class name), but they can be contained within an evaluable node (e.g. static member access)

The validation is modeled with a [Validation](https://github.com/scalacenter/scala-debug-adapter/blob/main/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/Validation.scala) monad:

- `Valid`: the expression is valid
- `Recoverable`: the expression is not valid, but we might be able to recover from it (e.g. when validating `foo`, we might not find a local variable `foo`, but we might find a field, a method, etc. that we can use instead)
- `CompilerRecoverable` when information at runtime is not enough to validate the information (e.g. overloads ambiguity at runtime), the expression compiler might be able to validate it
- `Fatal` for errors that cannot be recovered (e.g. parsing error, unexpected exception, etc.). *Abort the whole evaluation process*

#### Pre evaluation

Pre-evaluation is a validation mode that first validate an expression, and evaluate it does not have side effects / fail (only a few nodes can be pre-evaluated). It is used to access the runtime type of an expression to get more information about it. ([RuntimePreEvaluationValidator.scala](https://github.com/scalacenter/scala-debug-adapter/blob/main/modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/evaluator/RuntimePreEvaluationValidator.scala))
Copy link
Member

Choose a reason for hiding this comment

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

Something missing in this sentence:

 and evaluate it does not have side effects / fail 


### tests

The [`tests`](https://github.com/scalacenter/scala-debug-adapter/tree/main/modules/tests/src/test/scala/ch/epfl/scala/debugadapter) module contains the tests of the Scala Debug Server.
It uses the [TestingDebugClient](https://github.com/scalacenter/scala-debug-adapter/blob/main/modules/tests/src/main/scala/ch/epfl/scala/debugadapter/testfmk/TestingDebugClient.scala), a minimal debug client that is used to communicate with the debug server via a socket.

# References
### Show logs

To print the logs during the tests, you must change the logger in `DebugTestSuite#getDebugServer` and `DebugTestSuite#startDebugServer` from `NoopLogger` to `PrintLogger`

## References

- [Bloop Debugging Referece](https://scalacenter.github.io/bloop/docs/debugging-reference)
- [Bloop Debugging Reference](https://scalacenter.github.io/bloop/docs/debugging-reference)
- [Microsoft DAP for Java](https://github.com/microsoft/vscode-java-debug)
- [DebugAdapterProvider](https://github.com/build-server-protocol/build-server-protocol/issues/145)

# History
## History

- [Origial project discussion](https://github.com/scalameta/metals-feature-requests/issues/168)
- [Original project discussion](https://github.com/scalameta/metals-feature-requests/issues/168)
Loading