Skip to content

Commit 3ae2dbf

Browse files
authored
Add Stable Presentation Compiler (#17528)
This PR adds a stable presentation compiler implementation to dotty. This will ensure that each future version of Scala 3 is shipped with presentation compiler compiled against it, guaranteeing that IDE will support it. It will also improve support for projects relying on nonbootstrapped compiler such as scaladoc or dotty-language-server, as it is now possible to easily publish presentation compiler for those versions from dotty repository. It also adds vast of tests suits ported from metals, which will also help to detect unintended changes before they are merged. More information about this initiative can be found here: https://contributors.scala-lang.org/t/stable-presentation-compiler-api/6139
1 parent e97fea5 commit 3ae2dbf

File tree

135 files changed

+29662
-51
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

135 files changed

+29662
-51
lines changed

NOTICE.md

+8-4
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,19 @@ major authors were omitted by oversight.
8989
details.
9090

9191
* dotty.tools.dotc.coverage: Coverage instrumentation utilities have been
92-
adapted from the scoverage plugin for scala 2 [5], which is under the
92+
adapted from the scoverage plugin for scala 2 [4], which is under the
9393
Apache 2.0 license.
9494

95+
* dooty.tools.pc: Presentation compiler implementation adapted from
96+
scalameta/metals [5] mtags module, which is under the Apache 2.0 license.
97+
9598
* The Dotty codebase contains parts which are derived from
96-
the ScalaPB protobuf library [4], which is under the Apache 2.0 license.
99+
the ScalaPB protobuf library [6], which is under the Apache 2.0 license.
97100

98101

99102
[1] https://github.com/scala/scala
100103
[2] https://github.com/adriaanm/scala/tree/sbt-api-consolidate/src/compiler/scala/tools/sbt
101104
[3] https://github.com/sbt/sbt/tree/0.13/compile/interface/src/main/scala/xsbt
102-
[4] https://github.com/lampepfl/dotty/pull/5783/files
103-
[5] https://github.com/scoverage/scalac-scoverage-plugin
105+
[4] https://github.com/scoverage/scalac-scoverage-plugin
106+
[5] https://github.com/scalameta/metals
107+
[6] https://github.com/lampepfl/dotty/pull/5783/files

build.sbt

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ val `scala3-bench-run` = Build.`scala3-bench-run`
2929
val dist = Build.dist
3030
val `community-build` = Build.`community-build`
3131
val `sbt-community-build` = Build.`sbt-community-build`
32+
val `scala3-presentation-compiler` = Build.`scala3-presentation-compiler`
33+
val `scala3-presentation-compiler-bootstrapped` = Build.`scala3-presentation-compiler-bootstrapped`
3234

3335
val sjsSandbox = Build.sjsSandbox
3436
val sjsJUnitTests = Build.sjsJUnitTests

compiler/src/dotty/tools/dotc/CompilationUnit.scala

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import core._
55
import Contexts._
66
import SymDenotations.ClassDenotation
77
import Symbols._
8+
import Comments.Comment
89
import util.{FreshNameCreator, SourceFile, NoSource}
910
import util.Spans.Span
1011
import ast.{tpd, untpd}
@@ -69,6 +70,9 @@ class CompilationUnit protected (val source: SourceFile) {
6970
/** Can this compilation unit be suspended */
7071
def isSuspendable: Boolean = true
7172

73+
/** List of all comments present in this compilation unit */
74+
var comments: List[Comment] = Nil
75+
7276
/** Suspends the compilation unit by thowing a SuspendException
7377
* and recording the suspended compilation unit
7478
*/

compiler/src/dotty/tools/dotc/interactive/InteractiveDriver.scala

+2-5
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ class InteractiveDriver(val settings: List[String]) extends Driver {
145145
(fromSource ++ fromClassPath).distinct
146146
}
147147

148-
def run(uri: URI, sourceCode: String): List[Diagnostic] = run(uri, toSource(uri, sourceCode))
148+
def run(uri: URI, sourceCode: String): List[Diagnostic] = run(uri, SourceFile.virtual(uri, sourceCode))
149149

150150
def run(uri: URI, source: SourceFile): List[Diagnostic] = {
151151
import typer.ImportInfo._
@@ -297,9 +297,6 @@ class InteractiveDriver(val settings: List[String]) extends Driver {
297297
cleanupTree(tree)
298298
}
299299

300-
private def toSource(uri: URI, sourceCode: String): SourceFile =
301-
SourceFile.virtual(Paths.get(uri).toString, sourceCode)
302-
303300
/**
304301
* Initialize this driver and compiler.
305302
*
@@ -323,7 +320,7 @@ object InteractiveDriver {
323320
else
324321
try
325322
// We don't use file.file here since it'll be null
326-
// for the VirtualFiles created by InteractiveDriver#toSource
323+
// for the VirtualFiles created by SourceFile#virtual
327324
// TODO: To avoid these round trip conversions, we could add an
328325
// AbstractFile#toUri method and implement it by returning a constant
329326
// passed as a parameter to a constructor of VirtualFile

compiler/src/dotty/tools/dotc/parsing/ParserPhase.scala

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class Parser extends Phase {
3030
val p = new Parsers.Parser(unit.source)
3131
// p.in.debugTokenStream = true
3232
val tree = p.parse()
33+
ctx.compilationUnit.comments = p.in.comments
3334
if (p.firstXmlPos.exists && !firstXmlPos.exists)
3435
firstXmlPos = p.firstXmlPos
3536
tree

compiler/src/dotty/tools/dotc/parsing/Scanners.scala

+11-11
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,11 @@ object Scanners {
227227
*/
228228
private var docstringMap: SortedMap[Int, Comment] = SortedMap.empty
229229

230-
/* A Buffer for comment positions */
231-
private val commentPosBuf = new mutable.ListBuffer[Span]
230+
/* A Buffer for comments */
231+
private val commentBuf = new mutable.ListBuffer[Comment]
232232

233-
/** Return a list of all the comment positions */
234-
def commentSpans: List[Span] = commentPosBuf.toList
233+
/** Return a list of all the comments */
234+
def comments: List[Comment] = commentBuf.toList
235235

236236
private def addComment(comment: Comment): Unit = {
237237
val lookahead = lookaheadReader()
@@ -246,7 +246,7 @@ object Scanners {
246246
def getDocComment(pos: Int): Option[Comment] = docstringMap.get(pos)
247247

248248
/** A buffer for comments */
249-
private val commentBuf = CharBuffer(initialCharBufferSize)
249+
private val currentCommentBuf = CharBuffer(initialCharBufferSize)
250250

251251
def toToken(identifier: SimpleName): Token =
252252
def handleMigration(keyword: Token): Token =
@@ -523,7 +523,7 @@ object Scanners {
523523
*
524524
* The following tokens can start an indentation region:
525525
*
526-
* : = => <- if then else while do try catch
526+
* : = => <- if then else while do try catch
527527
* finally for yield match throw return with
528528
*
529529
* Inserting an INDENT starts a new indentation region with the indentation of the current
@@ -1019,7 +1019,7 @@ object Scanners {
10191019

10201020
private def skipComment(): Boolean = {
10211021
def appendToComment(ch: Char) =
1022-
if (keepComments) commentBuf.append(ch)
1022+
if (keepComments) currentCommentBuf.append(ch)
10231023
def nextChar() = {
10241024
appendToComment(ch)
10251025
Scanner.this.nextChar()
@@ -1047,9 +1047,9 @@ object Scanners {
10471047
def finishComment(): Boolean = {
10481048
if (keepComments) {
10491049
val pos = Span(start, charOffset - 1, start)
1050-
val comment = Comment(pos, commentBuf.toString)
1051-
commentBuf.clear()
1052-
commentPosBuf += pos
1050+
val comment = Comment(pos, currentCommentBuf.toString)
1051+
currentCommentBuf.clear()
1052+
commentBuf += comment
10531053

10541054
if (comment.isDocComment)
10551055
addComment(comment)
@@ -1065,7 +1065,7 @@ object Scanners {
10651065
else if (ch == '*') { nextChar(); skipComment(); finishComment() }
10661066
else {
10671067
// This was not a comment, remove the `/` from the buffer
1068-
commentBuf.clear()
1068+
currentCommentBuf.clear()
10691069
false
10701070
}
10711071
}

compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala

+11-11
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ class PlainPrinter(_ctx: Context) extends Printer {
179179
if (printWithoutPrefix.contains(tp.symbol))
180180
toText(tp.name)
181181
else
182-
toTextPrefix(tp.prefix) ~ selectionString(tp)
182+
toTextPrefixOf(tp) ~ selectionString(tp)
183183
case tp: TermParamRef =>
184184
ParamRefNameString(tp) ~ lambdaHash(tp.binder) ~ ".type"
185185
case tp: TypeParamRef =>
@@ -353,7 +353,7 @@ class PlainPrinter(_ctx: Context) extends Printer {
353353
def toTextRef(tp: SingletonType): Text = controlled {
354354
tp match {
355355
case tp: TermRef =>
356-
toTextPrefix(tp.prefix) ~ selectionString(tp)
356+
toTextPrefixOf(tp) ~ selectionString(tp)
357357
case tp: ThisType =>
358358
nameString(tp.cls) + ".this"
359359
case SuperType(thistpe: SingletonType, _) =>
@@ -375,15 +375,6 @@ class PlainPrinter(_ctx: Context) extends Printer {
375375
}
376376
}
377377

378-
/** The string representation of this type used as a prefix, including separator */
379-
def toTextPrefix(tp: Type): Text = controlled {
380-
homogenize(tp) match {
381-
case NoPrefix => ""
382-
case tp: SingletonType => toTextRef(tp) ~ "."
383-
case tp => trimPrefix(toTextLocal(tp)) ~ "#"
384-
}
385-
}
386-
387378
def toTextCaptureRef(tp: Type): Text =
388379
homogenize(tp) match
389380
case tp: TermRef if tp.symbol == defn.captureRoot => Str("cap")
@@ -393,6 +384,15 @@ class PlainPrinter(_ctx: Context) extends Printer {
393384
protected def isOmittablePrefix(sym: Symbol): Boolean =
394385
defn.unqualifiedOwnerTypes.exists(_.symbol == sym) || isEmptyPrefix(sym)
395386

387+
/** The string representation of type prefix, including separator */
388+
def toTextPrefixOf(tp: NamedType): Text = controlled {
389+
homogenize(tp.prefix) match {
390+
case NoPrefix => ""
391+
case tp: SingletonType => toTextRef(tp) ~ "."
392+
case tp => trimPrefix(toTextLocal(tp)) ~ "#"
393+
}
394+
}
395+
396396
protected def isEmptyPrefix(sym: Symbol): Boolean =
397397
sym.isEffectiveRoot || sym.isAnonymousClass || sym.name.isReplWrapperName
398398

compiler/src/dotty/tools/dotc/printing/Printer.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ package printing
44

55
import core._
66
import Texts._, ast.Trees._
7-
import Types.{Type, SingletonType, LambdaParam},
7+
import Types.{Type, SingletonType, LambdaParam, NamedType},
88
Symbols.Symbol, Scopes.Scope, Constants.Constant,
99
Names.Name, Denotations._, Annotations.Annotation, Contexts.Context
1010
import typer.Implicits.SearchResult
@@ -101,7 +101,7 @@ abstract class Printer {
101101
def toTextRef(tp: SingletonType): Text
102102

103103
/** Textual representation of a prefix of some reference, ending in `.` or `#` */
104-
def toTextPrefix(tp: Type): Text
104+
def toTextPrefixOf(tp: NamedType): Text
105105

106106
/** Textual representation of a reference in a capture set */
107107
def toTextCaptureRef(tp: Type): Text

compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala

+10-10
Original file line numberDiff line numberDiff line change
@@ -117,21 +117,22 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
117117
}
118118
}
119119

120-
override def toTextPrefix(tp: Type): Text = controlled {
120+
override def toTextPrefixOf(tp: NamedType): Text = controlled {
121121
def isOmittable(sym: Symbol) =
122122
if printDebug then false
123123
else if homogenizedView then isEmptyPrefix(sym) // drop <root> and anonymous classes, but not scala, Predef.
124124
else if sym.isPackageObject then isOmittablePrefix(sym.owner)
125125
else isOmittablePrefix(sym)
126-
tp match {
127-
case tp: ThisType if isOmittable(tp.cls) =>
126+
127+
tp.prefix match {
128+
case thisType: ThisType if isOmittable(thisType.cls) =>
128129
""
129-
case tp @ TermRef(pre, _) =>
130-
val sym = tp.symbol
131-
if sym.isPackageObject && !homogenizedView && !printDebug then toTextPrefix(pre)
130+
case termRef @ TermRef(pre, _) =>
131+
val sym = termRef.symbol
132+
if sym.isPackageObject && !homogenizedView && !printDebug then toTextPrefixOf(termRef)
132133
else if (isOmittable(sym)) ""
133-
else super.toTextPrefix(tp)
134-
case _ => super.toTextPrefix(tp)
134+
else super.toTextPrefixOf(tp)
135+
case _ => super.toTextPrefixOf(tp)
135136
}
136137
}
137138

@@ -427,8 +428,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
427428
case id @ Ident(name) =>
428429
val txt = tree.typeOpt match {
429430
case tp: NamedType if name != nme.WILDCARD =>
430-
val pre = if (tp.symbol.is(JavaStatic)) tp.prefix.widen else tp.prefix
431-
toTextPrefix(pre) ~ withPos(selectionString(tp), tree.sourcePos)
431+
toTextPrefixOf(tp) ~ withPos(selectionString(tp), tree.sourcePos)
432432
case _ =>
433433
toText(name)
434434
}

compiler/src/dotty/tools/dotc/printing/SyntaxHighlighting.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ object SyntaxHighlighting {
8383
}
8484
}
8585

86-
for (span <- scanner.commentSpans)
87-
highlightPosition(span, CommentColor)
86+
for (comment <- scanner.comments)
87+
highlightPosition(comment.span, CommentColor)
8888

8989
object TreeHighlighter extends untpd.UntypedTreeTraverser {
9090
import untpd._

compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ trait ImportSuggestions:
330330
def importString(ref: TermRef): String =
331331
val imported =
332332
if ref.symbol.is(ExtensionMethod) then
333-
s"${ctx.printer.toTextPrefix(ref.prefix).show}${ref.symbol.name}"
333+
s"${ctx.printer.toTextPrefixOf(ref).show}${ref.symbol.name}"
334334
else
335335
ctx.printer.toTextRef(ref).show
336336
s" import $imported"

compiler/src/dotty/tools/dotc/util/DiffUtil.scala

+67-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ object DiffUtil {
7070
* differences are highlighted.
7171
*/
7272
def mkColoredLineDiff(expected: Seq[String], actual: Seq[String]): String = {
73-
val expectedSize = EOF.length max expected.maxBy(_.length).length
73+
val longestExpected = expected.map(_.length).maxOption.getOrElse(0)
74+
val longestActual = actual.map(_.length).maxOption.getOrElse(0)
75+
val expectedSize = EOF.length max longestActual max longestExpected
7476
actual.padTo(expected.length, "").zip(expected.padTo(actual.length, "")).map { case (act, exp) =>
7577
mkColoredLineDiff(exp, act, expectedSize)
7678
}.mkString(System.lineSeparator)
@@ -101,11 +103,75 @@ object DiffUtil {
101103
case Deleted(str) => deleted(str)
102104
}.mkString
103105

106+
(expectedDiff, actualDiff)
104107
val pad = " " * 0.max(expectedSize - expected.length)
105108

106109
expectedDiff + pad + " | " + actualDiff
107110
}
108111

112+
private def ensureLineSeparator(str: String): String =
113+
if str.endsWith(System.lineSeparator) then
114+
str
115+
else
116+
str + System.lineSeparator
117+
118+
/**
119+
* Returns a colored diffs by comparison of lines instead of tokens.
120+
* It will automatically group subsequential pairs of `Insert` and `Delete`
121+
* in order to improve the readability
122+
*
123+
* @param expected The expected lines
124+
* @param actual The actual lines
125+
* @return A string with colored diffs between `expected` and `actual` grouped whenever possible
126+
*/
127+
def mkColoredHorizontalLineDiff(expected: String, actual: String): String = {
128+
val indent = 2
129+
val tab = " " * indent
130+
val insertIndent = "+" ++ (" " * (indent - 1))
131+
val deleteIndent = "-" ++ (" " * (indent - 1))
132+
133+
if actual.isEmpty then
134+
(expected.linesIterator.map(line => added(insertIndent + line)).toList :+ deleted("--- EMPTY OUTPUT ---"))
135+
.map(ensureLineSeparator).mkString
136+
else if expected.isEmpty then
137+
(added("--- NO VALUE EXPECTED ---") +: actual.linesIterator.map(line => deleted(deleteIndent + line)).toList)
138+
.map(ensureLineSeparator).mkString
139+
else
140+
lazy val diff = {
141+
val expectedTokens = expected.linesWithSeparators.toArray
142+
val actualTokens = actual.linesWithSeparators.toArray
143+
hirschberg(actualTokens, expectedTokens)
144+
}.toList
145+
146+
val transformedDiff = diff.flatMap {
147+
case Modified(original, str) => Seq(
148+
Inserted(ensureLineSeparator(original)), Deleted(ensureLineSeparator(str))
149+
)
150+
case other => Seq(other)
151+
}
152+
153+
val zipped = transformedDiff zip transformedDiff.drop(1)
154+
155+
val (acc, inserts, deletions) = zipped.foldLeft((Seq[Patch](), Seq[Inserted](), Seq[Deleted]())): (acc, patches) =>
156+
val (currAcc, inserts, deletions) = acc
157+
patches match
158+
case (currentPatch: Inserted, nextPatch: Deleted) =>
159+
(currAcc, inserts :+ currentPatch, deletions)
160+
case (currentPatch: Deleted, nextPatch: Inserted) =>
161+
(currAcc, inserts, deletions :+ currentPatch)
162+
case (currentPatch, nextPatch) =>
163+
(currAcc :++ inserts :++ deletions :+ currentPatch, Seq.empty, Seq.empty)
164+
165+
val stackedDiff = acc :++ inserts :++ deletions :+ diff.last
166+
167+
stackedDiff.collect {
168+
case Unmodified(str) => tab + str
169+
case Inserted(str) => added(insertIndent + str)
170+
case Deleted(str) => deleted(deleteIndent + str)
171+
}.map(ensureLineSeparator).mkString
172+
173+
}
174+
109175
def mkColoredCodeDiff(code: String, lastCode: String, printDiffDel: Boolean): String = {
110176
val tokens = splitTokens(code, Nil).toArray
111177
val lastTokens = splitTokens(lastCode, Nil).toArray

compiler/src/dotty/tools/dotc/util/SourceFile.scala

+9-1
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ import scala.collection.mutable.ArrayBuffer
1616
import scala.util.chaining.given
1717

1818
import java.io.File.separator
19+
import java.net.URI
1920
import java.nio.charset.StandardCharsets
20-
import java.nio.file.{FileSystemException, NoSuchFileException}
21+
import java.nio.file.{FileSystemException, NoSuchFileException, Paths}
2122
import java.util.Optional
2223
import java.util.concurrent.atomic.AtomicInteger
2324
import java.util.regex.Pattern
@@ -222,6 +223,13 @@ object SourceFile {
222223
SourceFile(new VirtualFile(name.replace(separator, "/"), content.getBytes(StandardCharsets.UTF_8)), content.toCharArray)
223224
.tap(_._maybeInComplete = maybeIncomplete)
224225

226+
/** A helper method to create a virtual source file for given URI.
227+
* It relies on SourceFile#virtual implementation to create the virtual file.
228+
*/
229+
def virtual(uri: URI, content: String): SourceFile =
230+
val path = Paths.get(uri).toString
231+
SourceFile.virtual(path, content)
232+
225233
/** Returns the relative path of `source` within the `reference` path
226234
*
227235
* It returns the absolute path of `source` if it is not contained in `reference`.

0 commit comments

Comments
 (0)