Skip to content

Commit

Permalink
backend computes line number from source of position (#21763)
Browse files Browse the repository at this point in the history
fixes #21762 

This makes it possible to implement line number correction for Mill
build files under Scala 3
smarter authored Oct 16, 2024
2 parents f6bfa0a + 44ecf4b commit 8431d84
Showing 10 changed files with 138 additions and 5 deletions.
8 changes: 7 additions & 1 deletion compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala
Original file line number Diff line number Diff line change
@@ -623,7 +623,13 @@ trait BCodeSkelBuilder extends BCodeHelpers {
}

if (emitLines && tree.span.exists && !tree.hasAttachment(SyntheticUnit)) {
val nr = ctx.source.offsetToLine(tree.span.point) + 1
val nr =
val sourcePos = tree.sourcePos
(
if sourcePos.exists then sourcePos.source.positionInUltimateSource(sourcePos).line
else ctx.source.offsetToLine(tree.span.point) // fallback
) + 1

if (nr != lastEmittedLineNr) {
lastEmittedLineNr = nr
getNonLabelNode(lastInsn) match {
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/util/SourceFile.scala
Original file line number Diff line number Diff line change
@@ -119,7 +119,8 @@ class SourceFile(val file: AbstractFile, computeContent: => Array[Char]) extends
* For regular source files, simply return the argument.
*/
def positionInUltimateSource(position: SourcePosition): SourcePosition =
SourcePosition(underlying, position.span shift start)
if isSelfContained then position // return the argument
else SourcePosition(underlying, position.span shift start)

private def calculateLineIndicesFromContents() = {
val cs = content()
1 change: 0 additions & 1 deletion compiler/src/dotty/tools/dotc/util/SourcePosition.scala
Original file line number Diff line number Diff line change
@@ -79,7 +79,6 @@ extends SrcPos, interfaces.SourcePosition, Showable {
rec(this)
}


override def toString: String =
s"${if (source.exists) source.file.toString else "(no source)"}:$span"

Original file line number Diff line number Diff line change
@@ -193,15 +193,18 @@ class BootstrappedOnlyCompilationTests {

// 1. hack with absolute path for -Xplugin
// 2. copy `pluginFile` to destination
def compileFilesInDir(dir: String): CompilationTest = {
def compileFilesInDir(dir: String, run: Boolean = false): CompilationTest = {
val outDir = defaultOutputDir + "testPlugins/"
val sourceDir = new java.io.File(dir)

val dirs = sourceDir.listFiles.toList.filter(_.isDirectory)
val targets = dirs.map { dir =>
val compileDir = createOutputDirsForDir(dir, sourceDir, outDir)
Files.copy(dir.toPath.resolve(pluginFile), compileDir.toPath.resolve(pluginFile), StandardCopyOption.REPLACE_EXISTING)
val flags = TestFlags(withCompilerClasspath, noCheckOptions).and("-Xplugin:" + compileDir.getAbsolutePath)
val flags = {
val base = TestFlags(withCompilerClasspath, noCheckOptions).and("-Xplugin:" + compileDir.getAbsolutePath)
if run then base.withRunClasspath(withCompilerClasspath) else base
}
SeparateCompilationSource("testPlugins", dir, flags, compileDir)
}

@@ -210,6 +213,7 @@ class BootstrappedOnlyCompilationTests {

compileFilesInDir("tests/plugins/neg").checkExpectedErrors()
compileDir("tests/plugins/custom/analyzer", withCompilerOptions.and("-Yretain-trees")).checkCompile()
compileFilesInDir("tests/plugins/run", run = true).checkRuns()
}
}

3 changes: 3 additions & 0 deletions tests/plugins/run/scriptWrapper/Framework_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package framework

class entrypoint extends scala.annotation.Annotation
68 changes: 68 additions & 0 deletions tests/plugins/run/scriptWrapper/LineNumberPlugin_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package scriptWrapper

import dotty.tools.dotc.*
import core.*
import Contexts.Context
import Contexts.ctx
import plugins.*
import ast.tpd
import util.SourceFile

class LineNumberPlugin extends StandardPlugin {
val name: String = "linenumbers"
val description: String = "adjusts line numbers of script files"

override def initialize(options: List[String])(using Context): List[PluginPhase] = FixLineNumbers() :: Nil
}

// Loosely follows Mill linenumbers plugin (scan for marker with "original" source, adjust line numbers to match)
class FixLineNumbers extends PluginPhase {

val codeMarker = "//USER_CODE_HERE"

def phaseName: String = "fixLineNumbers"
override def runsAfter: Set[String] = Set("posttyper")
override def runsBefore: Set[String] = Set("pickler")

override def transformUnit(tree: tpd.Tree)(using Context): tpd.Tree = {
val sourceContent = ctx.source.content()
val lines = new String(sourceContent).linesWithSeparators.toVector
val codeMarkerLine = lines.indexWhere(_.startsWith(codeMarker))

if codeMarkerLine < 0 then
tree
else
val adjustedFile = lines.collectFirst {
case s"//USER_SRC_FILE:./$file" => file.trim
}.getOrElse("<unknown>")

val adjustedSrc = ctx.source.file.container.lookupName(adjustedFile, directory = false) match
case null =>
report.error(s"could not find file $adjustedFile", tree.sourcePos)
return tree
case file =>
SourceFile(file, scala.io.Codec.UTF8)

val userCodeOffset = ctx.source.lineToOffset(codeMarkerLine + 1) // lines.take(codeMarkerLine).map(_.length).sum
val lineMapper = LineMapper(codeMarkerLine, userCodeOffset, adjustedSrc)
lineMapper.transform(tree)
}

}

class LineMapper(markerLine: Int, userCodeOffset: Int, adjustedSrc: SourceFile) extends tpd.TreeMapWithPreciseStatContexts() {

override def transform(tree: tpd.Tree)(using Context): tpd.Tree = {
val tree0 = super.transform(tree)
val pos = tree0.sourcePos
if pos.exists && pos.start >= userCodeOffset then
val tree1 = tree0.cloneIn(adjustedSrc).withSpan(pos.span.shift(-userCodeOffset))
// if tree1.show.toString == "???" then
// val pos1 = tree1.sourcePos
// sys.error(s"rewrote ??? at ${pos1.source}:${pos1.line + 1}:${pos1.column + 1} (sourced from ${markerLine + 2})")
tree1
else
tree0
}

}
25 changes: 25 additions & 0 deletions tests/plugins/run/scriptWrapper/Test_3.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@main def Test: Unit = {
val mainCls = Class.forName("foo_sc")
val mainMethod = mainCls.getMethod("main", classOf[Array[String]])
val stackTrace: Array[String] = {
try
mainMethod.invoke(null, Array.empty[String])
sys.error("Expected an exception")
catch
case e: java.lang.reflect.InvocationTargetException =>
val cause = e.getCause
if cause != null then
cause.getStackTrace.map(_.toString)
else
throw e
}

val expected = Set(
"foo_sc$.getRandom(foo_2.scala:3)", // adjusted line number (11 -> 3)
"foo_sc$.brokenRandom(foo_2.scala:5)", // adjusted line number (13 -> 5)
"foo_sc$.run(foo_2.scala:8)", // adjusted line number (16 -> 8)
)

val missing = expected -- stackTrace
assert(missing.isEmpty, s"Missing: $missing")
}
18 changes: 18 additions & 0 deletions tests/plugins/run/scriptWrapper/foo_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// generated code
// script: foo.sc
object foo_sc {
def main(args: Array[String]): Unit = {
run // assume some macro generates this by scanning for @entrypoint
}
//USER_SRC_FILE:./foo_original_2.scala
//USER_CODE_HERE
import framework.*

def getRandom: Int = brokenRandom // LINE 3;

def brokenRandom: Int = ??? // LINE 5;

@entrypoint
def run = println("Hello, here is a random number: " + getRandom) // LINE 8;
//END_USER_CODE_HERE
}
8 changes: 8 additions & 0 deletions tests/plugins/run/scriptWrapper/foo_original_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import framework.*

def getRandom: Int = brokenRandom // LINE 3;

def brokenRandom: Int = ??? // LINE 5;

@entrypoint
def run = println("Hello, here is a random number: " + getRandom) // LINE 8;
1 change: 1 addition & 0 deletions tests/plugins/run/scriptWrapper/plugin.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pluginClass=scriptWrapper.LineNumberPlugin

0 comments on commit 8431d84

Please sign in to comment.