Skip to content

JSR-45 #15684

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

Closed
wants to merge 20 commits into from
Closed

JSR-45 #15684

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e313ef9
Generate SMAPs for files with inlined calls
Kordyjan Feb 3, 2021
1b31c9d
Include duplicates of inlined calls in SMAP
Kordyjan Feb 11, 2021
37ceb24
Add missing info to SMAP header
Kordyjan Feb 11, 2021
220eddc
Update information about lines for inlined content
Kordyjan Feb 15, 2021
0fab476
Make InlinePositioner not crash on not found inline request
Kordyjan Feb 15, 2021
dcc22f8
Solve not registering inlining position for tuples
Kordyjan Feb 21, 2021
87c8708
Refactor InlinedsPositioner into InlinedSourceMap
Kordyjan Feb 21, 2021
3d73cb3
Filter out information about inlining in the same file
Kordyjan Feb 21, 2021
0ef53d5
Add information aobut internal class name to SMAP
Kordyjan Feb 21, 2021
a4d74b6
Fix problems with only first instance of inlined call being found
Kordyjan Feb 21, 2021
a178e74
Disable YCheckPosition
Kordyjan Feb 21, 2021
6d892b5
Add explanation to InlinedSourceMaps
Kordyjan Feb 21, 2021
2c5b93a
Skip inline proxies during accessibility assertions
Kordyjan Mar 22, 2021
e373171
Sort inlining requests before creating SMAP
Kordyjan Mar 24, 2021
26566a0
Adapt Inliner to the most recent refactoring on the `main`
anatoliykmetyuk Jul 15, 2022
d8ed01f
Comment out warning about inline positions recording
anatoliykmetyuk Jul 15, 2022
ad6c699
Don't generate mappings for the quotes compiled at runtime by the sta…
anatoliykmetyuk Aug 12, 2022
aec1311
Inlined code gets virtual line numbers in stack traces
anatoliykmetyuk Aug 12, 2022
28e3e32
Fix virtual line numbers in assert-stack
anatoliykmetyuk Aug 12, 2022
c09374f
Don't do ExpandPrivate checks on synthetic methods.
anatoliykmetyuk Aug 26, 2022
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
32 changes: 24 additions & 8 deletions compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
166 changes: 166 additions & 0 deletions compiler/src/dotty/tools/backend/jvm/InlinedSourceMaps.scala
Original file line number Diff line number Diff line change
@@ -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


1 change: 0 additions & 1 deletion compiler/src/dotty/tools/dotc/Compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 8 additions & 53 deletions compiler/src/dotty/tools/dotc/inlines/Inlines.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand All @@ -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 =
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions tests/run-macros/i4947e.check
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions tests/run-macros/i4947f.check
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion tests/run/assert-stack.check
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Loading