diff --git a/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala b/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala index a524d5fb5a8b..d6af1163559e 100644 --- a/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala +++ b/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala @@ -24,6 +24,8 @@ import dotty.tools.dotc.util.Spans._ import dotty.tools.dotc.report import dotty.tools.dotc.transform.SymUtils._ +import InlinedSourceMaps._ + /* * * @author Miguel Garcia, http://lamp.epfl.ch/~magarcia/ScalaCompilerCornerReloaded/ @@ -91,6 +93,8 @@ trait BCodeSkelBuilder extends BCodeHelpers { var isCZParcelable = false var isCZStaticModule = false + var sourceMap: InlinedSourceMap = null + /* ---------------- idiomatic way to ask questions to typer ---------------- */ def paramTKs(app: Apply, take: Int = -1): List[BType] = app match { @@ -111,6 +115,7 @@ trait BCodeSkelBuilder extends BCodeHelpers { def genPlainClass(cd0: TypeDef) = cd0 match { case TypeDef(_, impl: Template) => + assert(cnode == null, "GenBCode detected nested methods.") claszSymbol = cd0.symbol @@ -276,7 +281,8 @@ trait BCodeSkelBuilder extends BCodeHelpers { superClass, interfaceNames.toArray) if (emitSource) { - cnode.visitSource(cunit.source.file.name, null /* SourceDebugExtension */) + sourceMap = sourceMapFor(cunit)(s => classBTypeFromSymbol(s).internalName) + cnode.visitSource(cunit.source.file.name, sourceMap.debugExtension.orNull) } enclosingMethodAttribute(claszSymbol, internalName, asmMethodType(_).descriptor) match { @@ -370,6 +376,8 @@ trait BCodeSkelBuilder extends BCodeHelpers { var shouldEmitCleanup = false // line numbers var lastEmittedLineNr = -1 + // by real line number we mean line number that is not pointing to virtual lines added by inlined calls + var lastRealLineNr = -1 object bc extends JCodeMethodN { override def jmethod = PlainSkelBuilder.this.mnode @@ -546,19 +554,27 @@ trait BCodeSkelBuilder extends BCodeHelpers { case labnode: asm.tree.LabelNode => (labnode.getLabel == lbl); case _ => false } ) } - def lineNumber(tree: Tree): Unit = { - if (!emitLines || !tree.span.exists) return; - val nr = ctx.source.offsetToLine(tree.span.point) + 1 - if (nr != lastEmittedLineNr) { + + def emitNr(nr: Int): Unit = + if nr != lastEmittedLineNr then lastEmittedLineNr = nr - lastInsn match { + lastInsn match case lnn: asm.tree.LineNumberNode => // overwrite previous landmark as no instructions have been emitted for it lnn.line = nr case _ => mnode.visitLineNumber(nr, currProgramPoint()) - } - } + + def lineNumber(tree: Tree): Unit = { + if !emitLines || !tree.span.exists then return; + if tree.source != cunit.source then + sourceMap.lineFor(tree.sourcePos, lastRealLineNr) match + case Some(nr) => emitNr(nr) + case None => () + else + val nr = ctx.source.offsetToLine(tree.span.point) + 1 + lastRealLineNr = nr + emitNr(nr) } // on entering a method diff --git a/compiler/src/dotty/tools/backend/jvm/InlinedSourceMaps.scala b/compiler/src/dotty/tools/backend/jvm/InlinedSourceMaps.scala new file mode 100644 index 000000000000..211c79b5eefd --- /dev/null +++ b/compiler/src/dotty/tools/backend/jvm/InlinedSourceMaps.scala @@ -0,0 +1,166 @@ +package dotty.tools +package backend +package jvm + +import dotc.CompilationUnit +import dotc.ast.tpd._ +import dotc.util.{ SourcePosition, SourceFile } +import dotc.core.Contexts._ +import dotc.core.Symbols.Symbol +import dotc.report +import dotc.inlines.Inlines.InliningPosition +import collection.mutable + +/** + * Tool for generating virtual lines for inlined calls and keeping track of them. + + * How it works: + * - For every inlined call it assumes that empty lines are appended to the source file. These + * lines are not added anywhere in physical form. We only assume that they exist only to be used + * by `LineNumberTable` and `SourceDebugExtension`. The number of these virtual lines is every + * time equal to the size of line range of the expansion of inlined call. + * - It generates SMAP (as defined by JSR-45) containing two strata. The first stratum (`Scala`) + * is describing the mapping from the real source files to the real and virtual lines in our + * assumed source. The second stratum (`ScalaDebug`) is mapping from virtual lines to + * corresponding inlined calls. + * - Generated SMAP is written to the bytecode in `SourceDebugExtension` + * - During the generation of the bytecode backed is asking `InlinedSourceMap` about position of + * all trees that have source different from the main source of given compilation unit. + * The response to that request is number of the virtual line that is corresponding to particular + * line from the other source. + * - Debuggers can use information stored in `LineNumberTable` and `SourceDebugExtension` to + * correctly guess which line of inlined method is currently executed. They can also construct + * stack frames for inlined calls. + **/ +object InlinedSourceMaps: + private case class Request(targetPos: SourcePosition, origPos: SourcePosition, firstFakeLine: Int) + + private class File(id: Int, name: String, path: Option[String]): + def write(b: mutable.StringBuilder): Unit = + if path.isDefined then b ++= "+ " + b append id + b += ' ' + b ++= name + b += '\n' + path.foreach { p => + b ++= p + b += '\n' + } + end File + + private class Mapping( + inputStartLine: Int, + fileId: Int, + repeatCount: Int, + outputStartLine: Int, + increment: Int + ): + extension (b: mutable.StringBuilder) def appendNotDefault(prefix: Char, value: Int): Unit = + if value != 1 then + b += prefix + b append value + + def write(b: mutable.StringBuilder): Unit = + b append (inputStartLine + 1) + b.appendNotDefault('#', fileId) + b.appendNotDefault(',', repeatCount) + b += ':' + b append (outputStartLine + 1) + b.appendNotDefault(',', increment) + b += '\n' + end Mapping + + private class Stratum(name: String, files: List[File], mappings: List[Mapping]): + def write(b: mutable.StringBuilder): Unit = + b ++= "*S " + b ++= name + b ++= "\n*F\n" + files.foreach(_.write(b)) + b ++= "*L\n" + mappings.foreach(_.write(b)) + b ++= "*E\n" + end Stratum + + def sourceMapFor(cunit: CompilationUnit)(internalNameProvider: Symbol => String)(using Context): InlinedSourceMap = + val requests = mutable.ListBuffer.empty[(SourcePosition, SourcePosition)] + var internalNames = Map.empty[SourceFile, String] + + class RequestCollector(enclosingFile: SourceFile) extends TreeTraverser: + override def traverse(tree: Tree)(using Context): Unit = + if tree.source != enclosingFile && tree.source != cunit.source then + tree.getAttachment(InliningPosition) match + case Some(InliningPosition(targetPos, cls)) => + requests += (targetPos -> tree.sourcePos) + + cls match + case Some(symbol) if !internalNames.isDefinedAt(tree.source) => + internalNames += (tree.source -> internalNameProvider(symbol)) + // We are skipping any internal name info if we already have one stored in our map + // because a debugger will use internal name only to localize matching source. + // Both old and new internal names are associated with the same source file + // so it doesn't matter if internal name is not matching used symbol. + case _ => () + RequestCollector(tree.source).traverseChildren(tree) + case None => + // Not exactly sure in which cases it is happening. Should we report warning? + RequestCollector(tree.source).traverseChildren(tree) + else traverseChildren(tree) + end RequestCollector + + // Don't generate mappings for the quotes compiled at runtime by the staging compiler + if cunit.source.file.isVirtual then InlinedSourceMap(cunit, Nil, Map.empty[SourceFile, String]) + else + var lastLine = cunit.tpdTree.sourcePos.endLine + def allocate(origPos: SourcePosition): Int = + val line = lastLine + 1 + lastLine += origPos.lines.length + line + + RequestCollector(cunit.source).traverse(cunit.tpdTree) + val allocated = requests.sortBy(_._1.start).map(r => Request(r._1, r._2, allocate(r._2))) + InlinedSourceMap(cunit, allocated.toList, internalNames) + end sourceMapFor + + class InlinedSourceMap private[InlinedSourceMaps] ( + cunit: CompilationUnit, + requests: List[Request], + internalNames: Map[SourceFile, String])(using Context): + + def debugExtension: Option[String] = Option.when(requests.nonEmpty) { + val scalaStratum = + val files = cunit.source :: requests.map(_.origPos.source).distinct.filter(_ != cunit.source) + val mappings = requests.map { case Request(_, origPos, firstFakeLine) => + Mapping(origPos.startLine, files.indexOf(origPos.source) + 1, origPos.lines.length, firstFakeLine, 1) + } + Stratum("Scala", + files.zipWithIndex.map { case (f, n) => File(n + 1, f.name, internalNames.get(f)) }, + Mapping(0, 1, cunit.tpdTree.sourcePos.lines.length, 0, 1) +: mappings + ) + + val debugStratum = + val mappings = requests.map { case Request(targetPos, origPos, firstFakeLine) => + Mapping(targetPos.startLine, 1, 1, firstFakeLine, origPos.lines.length) + } + Stratum("ScalaDebug", File(1, cunit.source.name, None) :: Nil, mappings) + + + val b = new StringBuilder + b ++= "SMAP\n" + b ++= cunit.source.name + b += '\n' + b ++= "Scala\n" + scalaStratum.write(b) + debugStratum.write(b) + b.toString + } + + def lineFor(sourcePos: SourcePosition, lastRealNr: Int): Option[Int] = + requests.find(r => r.origPos.contains(sourcePos) && r.targetPos.endLine + 1 >= lastRealNr) match + case Some(request) => + val offset = sourcePos.startLine - request.origPos.startLine + Some(request.firstFakeLine + offset + 1) + case None => + // report.warning(s"${sourcePos.show} was inlined in ${cunit.source} but its inlining position was not recorded.") + None + + diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index ce4ed2d4e4e8..d11a45be6131 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -34,7 +34,6 @@ class Compiler { protected def frontendPhases: List[List[Phase]] = List(new Parser) :: // Compiler frontend: scanner, parser List(new TyperPhase) :: // Compiler frontend: namer, typer - List(new YCheckPositions) :: // YCheck positions List(new sbt.ExtractDependencies) :: // Sends information on classes' dependencies to sbt via callbacks List(new semanticdb.ExtractSemanticDB) :: // Extract info into .semanticdb files List(new PostTyper) :: // Additional checks and cleanups after type checking diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index d1a88406fe45..c2d2bf4a0d90 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -11,7 +11,7 @@ import NameKinds.BodyRetainerName import SymDenotations.SymDenotation import config.Printers.inlining import ErrorReporting.errorTree -import dotty.tools.dotc.util.{SourceFile, SourcePosition, SrcPos} +import dotty.tools.dotc.util.{SourceFile, SourcePosition, SrcPos, Property} import parsing.Parsers.Parser import transform.{PostTyper, Inlining, CrossVersionChecks} @@ -28,6 +28,9 @@ object Inlines: */ private[dotc] class MissingInlineInfo extends Exception + object InliningPosition extends Property.StickyKey[InliningPosition] + case class InliningPosition(sourcePos: SourcePosition, topLevelSymbol: Option[Symbol]) + /** `sym` is an inline method with a known body to inline. */ def hasBodyToInline(sym: SymDenotation)(using Context): Boolean = @@ -246,58 +249,10 @@ object Inlines: /** Replace `Inlined` node by a block that contains its bindings and expansion */ def dropInlined(inlined: Inlined)(using Context): Tree = - val tree1 = - if inlined.bindings.isEmpty then inlined.expansion - else cpy.Block(inlined)(inlined.bindings, inlined.expansion) - // Reposition in the outer most inlined call - if (enclosingInlineds.nonEmpty) tree1 else reposition(tree1, inlined.span) - - def reposition(tree: Tree, callSpan: Span)(using Context): Tree = - // Reference test tests/run/i4947b - - val curSource = ctx.compilationUnit.source - - // Tree copier that changes the source of all trees to `curSource` - val cpyWithNewSource = new TypedTreeCopier { - override protected def sourceFile(tree: tpd.Tree): SourceFile = curSource - override protected val untpdCpy: untpd.UntypedTreeCopier = new untpd.UntypedTreeCopier { - override protected def sourceFile(tree: untpd.Tree): SourceFile = curSource - } - } - - /** Removes all Inlined trees, replacing them with blocks. - * Repositions all trees directly inside an inlined expansion of a non empty call to the position of the call. - * Any tree directly inside an empty call (inlined in the inlined code) retains their position. - * - * Until we implement JSR-45, we cannot represent in output positions in other source files. - * So, reposition inlined code from other files with the call position. - */ - class Reposition extends TreeMap(cpyWithNewSource) { - - override def transform(tree: Tree)(using Context): Tree = { - def fixSpan[T <: untpd.Tree](copied: T): T = - copied.withSpan(if tree.source == curSource then tree.span else callSpan) - def finalize(copied: untpd.Tree) = - fixSpan(copied).withAttachmentsFrom(tree).withTypeUnchecked(tree.tpe) - - inContext(ctx.withSource(curSource)) { - tree match - case tree: Ident => finalize(untpd.Ident(tree.name)(curSource)) - case tree: Literal => finalize(untpd.Literal(tree.const)(curSource)) - case tree: This => finalize(untpd.This(tree.qual)(curSource)) - case tree: JavaSeqLiteral => finalize(untpd.JavaSeqLiteral(transform(tree.elems), transform(tree.elemtpt))(curSource)) - case tree: SeqLiteral => finalize(untpd.SeqLiteral(transform(tree.elems), transform(tree.elemtpt))(curSource)) - case tree: Bind => finalize(untpd.Bind(tree.name, transform(tree.body))(curSource)) - case tree: TypeTree => finalize(tpd.TypeTree(tree.tpe)) - case tree: DefTree => super.transform(tree).setDefTree - case EmptyTree => tree - case _ => fixSpan(super.transform(tree)) - } - } - } - - (new Reposition).transform(tree) - end reposition + val topLevelClass = Option.when(!inlined.call.isEmpty)(inlined.call.symbol.topLevelClass) + val inliningPosition = InliningPosition(inlined.sourcePos, topLevelClass) + val withPos = inlined.expansion.withAttachment(InliningPosition, inliningPosition) + if inlined.bindings.isEmpty then withPos else cpy.Block(inlined)(inlined.bindings, withPos) /** Leave only a call trace consisting of * - a reference to the top-level class from which the call was inlined, diff --git a/compiler/src/dotty/tools/dotc/transform/ExpandPrivate.scala b/compiler/src/dotty/tools/dotc/transform/ExpandPrivate.scala index 41e5b76ca874..6aae841b7d92 100644 --- a/compiler/src/dotty/tools/dotc/transform/ExpandPrivate.scala +++ b/compiler/src/dotty/tools/dotc/transform/ExpandPrivate.scala @@ -66,7 +66,7 @@ class ExpandPrivate extends MiniPhase with IdentityDenotTransformer { thisPhase private def ensurePrivateAccessible(d: SymDenotation)(using Context) = if (isVCPrivateParamAccessor(d)) d.ensureNotPrivate.installAfter(thisPhase) - else if (d.is(PrivateTerm) && !d.owner.is(Package) && d.owner != ctx.owner.lexicallyEnclosingClass) { + else if (d.is(PrivateTerm) && !d.owner.is(Package) && d.owner != ctx.owner.lexicallyEnclosingClass && !d.is(InlineProxy) && !d.is(Synthetic)) { // Paths `p1` and `p2` are similar if they have a common suffix that follows // possibly different directory paths. That is, their common suffix extends // in both cases either to the start of the path or to a file separator character. diff --git a/tests/run-macros/i4947e.check b/tests/run-macros/i4947e.check index 1e67df692f1e..26435e45140a 100644 --- a/tests/run-macros/i4947e.check +++ b/tests/run-macros/i4947e.check @@ -1,8 +1,8 @@ -assertImpl: Test$.main(Test_2.scala:7) +assertImpl: Test$.main(Test_2.scala:16) true -assertImpl: Test$.main(Test_2.scala:8) +assertImpl: Test$.main(Test_2.scala:16) false -assertImpl: Test$.main(Test_2.scala:9) +assertImpl: Test$.main(Test_2.scala:18) hi: Test$.main(Test_2.scala:10) hi again: Test$.main(Test_2.scala:11) false diff --git a/tests/run-macros/i4947f.check b/tests/run-macros/i4947f.check index 1e67df692f1e..26435e45140a 100644 --- a/tests/run-macros/i4947f.check +++ b/tests/run-macros/i4947f.check @@ -1,8 +1,8 @@ -assertImpl: Test$.main(Test_2.scala:7) +assertImpl: Test$.main(Test_2.scala:16) true -assertImpl: Test$.main(Test_2.scala:8) +assertImpl: Test$.main(Test_2.scala:16) false -assertImpl: Test$.main(Test_2.scala:9) +assertImpl: Test$.main(Test_2.scala:18) hi: Test$.main(Test_2.scala:10) hi again: Test$.main(Test_2.scala:11) false diff --git a/tests/run/assert-stack.check b/tests/run/assert-stack.check index c2c97450fd57..6e773ed69d8b 100644 --- a/tests/run/assert-stack.check +++ b/tests/run/assert-stack.check @@ -2,5 +2,5 @@ scala.runtime.Scala3RunTime$.assertFailed(Scala3RunTime.scala:8) Test$.main(assert-stack.scala:7) scala.runtime.Scala3RunTime$.assertFailed(Scala3RunTime.scala:11) -Test$.main(assert-stack.scala:12) +Test$.main(assert-stack.scala:24) diff --git a/tests/run/i4947b.check b/tests/run/i4947b.check index 3950d4e0b7a1..e603f0d9ca79 100644 --- a/tests/run/i4947b.check +++ b/tests/run/i4947b.check @@ -1,36 +1,36 @@ -track: Test$.main(Test_2.scala:5) -track: Test$.main(Test_2.scala:5) +track: Test$.main(Test_2.scala:25) +track: Test$.main(Test_2.scala:26) main1: Test$.main(Test_2.scala:6) main2: Test$.main(Test_2.scala:7) -track: Test$.main(Test_2.scala:9) -track: Test$.main(Test_2.scala:9) -track: Test$.main(Test_2.scala:10) -track: Test$.main(Test_2.scala:10) +track: Test$.main(Test_2.scala:25) +track: Test$.main(Test_2.scala:26) +track: Test$.main(Test_2.scala:25) +track: Test$.main(Test_2.scala:26) main3: Test$.main(Test_2.scala:11) main4: Test$.main(Test_2.scala:12) track (i = 0): Test$.main(Test_2.scala:15) track (i = 0): Test$.main(Test_2.scala:15) -track: Test$.main(Test_2.scala:15) -track: Test$.main(Test_2.scala:15) -fact: Test$.main(Test_2.scala:15) +track: Test$.main(Test_2.scala:35) +track: Test$.main(Test_2.scala:36) +fact: Test$.main(Test_2.scala:43) track (i = 2): Test$.main(Test_2.scala:16) track (i = 2): Test$.main(Test_2.scala:16) -track: Test$.main(Test_2.scala:16) -track: Test$.main(Test_2.scala:16) -fact: Test$.main(Test_2.scala:16) +track: Test$.main(Test_2.scala:35) +track: Test$.main(Test_2.scala:36) +fact: Test$.main(Test_2.scala:43) main1 (i = -1): Test$.main(Test_2.scala:17) main2 (i = -1): Test$.main(Test_2.scala:18) -track (i = 1): Test$.main(Test_2.scala:16) -track (i = 1): Test$.main(Test_2.scala:16) -track: Test$.main(Test_2.scala:16) -track: Test$.main(Test_2.scala:16) -fact: Test$.main(Test_2.scala:16) +track (i = 1): Test$.main(Test_2.scala:49) +track (i = 1): Test$.main(Test_2.scala:49) +track: Test$.main(Test_2.scala:35) +track: Test$.main(Test_2.scala:36) +fact: Test$.main(Test_2.scala:43) main1 (i = -1): Test$.main(Test_2.scala:17) main2 (i = -1): Test$.main(Test_2.scala:18) -track (i = 0): Test$.main(Test_2.scala:16) -track (i = 0): Test$.main(Test_2.scala:16) -track: Test$.main(Test_2.scala:16) -track: Test$.main(Test_2.scala:16) -fact: Test$.main(Test_2.scala:16) +track (i = 0): Test$.main(Test_2.scala:49) +track (i = 0): Test$.main(Test_2.scala:49) +track: Test$.main(Test_2.scala:35) +track: Test$.main(Test_2.scala:36) +fact: Test$.main(Test_2.scala:43) main1 (i = -1): Test$.main(Test_2.scala:17) main2 (i = -1): Test$.main(Test_2.scala:18) diff --git a/tests/run/splice-position.check b/tests/run/splice-position.check index a37b1fbb806d..9e1489ab38f4 100644 --- a/tests/run/splice-position.check +++ b/tests/run/splice-position.check @@ -1,2 +1,2 @@ -Test$.main(Test.scala:5) -Test$.main(Test.scala:6) +Test$.main(Test.scala:7) +Test$.main(Test.scala:8)