Skip to content

Commit d8e50f2

Browse files
authored
Merge pull request #14683 from Xavientois/underline-safe-init-trace
Add underlining to safe-init stack traces
2 parents fdc31d2 + 4c75c8d commit d8e50f2

13 files changed

+96
-44
lines changed

compiler/src/dotty/tools/dotc/transform/init/Errors.scala

+21-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package init
55

66
import ast.tpd._
77
import core._
8+
import util.SourcePosition
89
import Decorators._, printing.SyntaxHighlighting
910
import Types._, Symbols._, Contexts._
1011

@@ -26,28 +27,44 @@ object Errors {
2627
def toErrors: Errors = this :: Nil
2728

2829
def stacktrace(using Context): String = if (trace.isEmpty) "" else " Calling trace:\n" + {
29-
var indentCount = 0
3030
var last: String = ""
3131
val sb = new StringBuilder
3232
trace.foreach { tree =>
33-
indentCount += 1
3433
val pos = tree.sourcePos
35-
val prefix = s"${ " " * indentCount }-> "
34+
val prefix = "-> "
3635
val line =
3736
if pos.source.exists then
3837
val loc = "[ " + pos.source.file.name + ":" + (pos.line + 1) + " ]"
3938
val code = SyntaxHighlighting.highlight(pos.lineContent.trim.nn)
4039
i"$code\t$loc"
4140
else
4241
tree.show
42+
val positionMarkerLine =
43+
if pos.exists && pos.source.exists then
44+
positionMarker(pos)
45+
else ""
4346

44-
if (last != line) sb.append(prefix + line + "\n")
47+
if (last != line) sb.append(prefix + line + "\n" + positionMarkerLine )
4548

4649
last = line
4750
}
4851
sb.toString
4952
}
5053

54+
/** Used to underline source positions in the stack trace
55+
* pos.source must exist
56+
*/
57+
private def positionMarker(pos: SourcePosition): String = {
58+
val trimmed = pos.lineContent.takeWhile(c => c.isWhitespace).length
59+
val padding = pos.startColumnPadding.substring(trimmed).nn + " "
60+
val carets =
61+
if (pos.startLine == pos.endLine)
62+
"^" * math.max(1, pos.endColumn - pos.startColumn)
63+
else "^"
64+
65+
s"$padding$carets\n"
66+
}
67+
5168
/** Flatten UnsafePromotion errors
5269
*/
5370
def flatten: Errors = this match {

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ class SourceFile(val file: AbstractFile, computeContent: => Array[Char]) extends
212212
var idx = startOfLine(offset)
213213
val pad = new StringBuilder
214214
while (idx != offset) {
215-
pad.append(if (idx < length && content()(idx) == '\t') '\t' else ' ')
215+
pad.append(if (idx < content().length && content()(idx) == '\t') '\t' else ' ')
216216
idx += 1
217217
}
218218
pad.result()

tests/init/neg/cycle-structure.check

+12-6
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@
22
2 | val x1 = b.x // error
33
| ^^^
44
| Access field A.this.b.x on a value with an unknown initialization status. Calling trace:
5-
| -> val x = A(this) [ cycle-structure.scala:9 ]
6-
| -> case class A(b: B) { [ cycle-structure.scala:1 ]
5+
| -> val x = A(this) [ cycle-structure.scala:9 ]
6+
| ^^^^^^^
7+
| -> case class A(b: B) { [ cycle-structure.scala:1 ]
8+
| ^
79
-- Error: tests/init/neg/cycle-structure.scala:8:15 --------------------------------------------------------------------
810
8 | val x1 = a.x // error
911
| ^^^
1012
| Access field B.this.a.x on a value with an unknown initialization status. Calling trace:
11-
| -> val x = A(this) [ cycle-structure.scala:9 ]
12-
| -> case class A(b: B) { [ cycle-structure.scala:1 ]
13-
| -> val x = B(this) [ cycle-structure.scala:3 ]
14-
| -> case class B(a: A) { [ cycle-structure.scala:7 ]
13+
| -> val x = A(this) [ cycle-structure.scala:9 ]
14+
| ^^^^^^^
15+
| -> case class A(b: B) { [ cycle-structure.scala:1 ]
16+
| ^
17+
| -> val x = B(this) [ cycle-structure.scala:3 ]
18+
| ^^^^^^^
19+
| -> case class B(a: A) { [ cycle-structure.scala:7 ]
20+
| ^

tests/init/neg/default-this.check

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
9 | compare() // error
33
| ^^^^^^^
44
|Cannot prove that the value is fully initialized. Only initialized values may be used as arguments. Calling trace:
5-
| -> val result = updateThenCompare(5) [ default-this.scala:11 ]
5+
|-> val result = updateThenCompare(5) [ default-this.scala:11 ]
6+
| ^^^^^^^^^^^^^^^^^^^^

tests/init/neg/enum-desugared.check

+8-4
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@
55
|
66
| The unsafe promotion may cause the following problem:
77
| Calling the external method method name may cause initialization errors. Calling trace:
8-
| -> Array(this.LazyErrorId, this.NoExplanationID) // error // error [ enum-desugared.scala:17 ]
9-
| -> override def productPrefix: String = this.name() [ enum-desugared.scala:29 ]
8+
| -> Array(this.LazyErrorId, this.NoExplanationID) // error // error [ enum-desugared.scala:17 ]
9+
| ^^^^^^^^^^^^^^^^
10+
| -> override def productPrefix: String = this.name() [ enum-desugared.scala:29 ]
11+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1012
-- Error: tests/init/neg/enum-desugared.scala:17:33 --------------------------------------------------------------------
1113
17 | Array(this.LazyErrorId, this.NoExplanationID) // error // error
1214
| ^^^^^^^^^^^^^^^^^^^^
1315
| Cannot prove that the value is fully initialized. May only use initialized value as method arguments.
1416
|
1517
| The unsafe promotion may cause the following problem:
1618
| Calling the external method method ordinal may cause initialization errors. Calling trace:
17-
| -> Array(this.LazyErrorId, this.NoExplanationID) // error // error [ enum-desugared.scala:17 ]
18-
| -> def errorNumber: Int = this.ordinal() - 2 [ enum-desugared.scala:8 ]
19+
| -> Array(this.LazyErrorId, this.NoExplanationID) // error // error [ enum-desugared.scala:17 ]
20+
| ^^^^^^^^^^^^^^^^^^^^
21+
| -> def errorNumber: Int = this.ordinal() - 2 [ enum-desugared.scala:8 ]
22+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

tests/init/neg/enum.check

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
|
66
| The unsafe promotion may cause the following problem:
77
| Calling the external method method name may cause initialization errors. Calling trace:
8-
| -> NoExplanationID // error [ enum.scala:4 ]
8+
| -> NoExplanationID // error [ enum.scala:4 ]
9+
| ^

tests/init/neg/inherit-non-hot.check

+16-8
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,22 @@
33
| ^^^^^^^^^^^
44
| Cannot prove that the value is fully initialized. May only assign fully initialized value.
55
| Calling trace:
6-
| -> val c = new C [ inherit-non-hot.scala:19 ]
7-
| -> class C extends A { [ inherit-non-hot.scala:15 ]
8-
| -> val bAgain = toB.getBAgain [ inherit-non-hot.scala:16 ]
6+
| -> val c = new C [ inherit-non-hot.scala:19 ]
7+
| ^^^^^
8+
| -> class C extends A { [ inherit-non-hot.scala:15 ]
9+
| ^
10+
| -> val bAgain = toB.getBAgain [ inherit-non-hot.scala:16 ]
11+
| ^^^
912
|
1013
| The unsafe promotion may cause the following problem:
1114
| Call method Foo.B.this.aCopy.toB on a value with an unknown initialization. Calling trace:
12-
| -> val c = new C [ inherit-non-hot.scala:19 ]
13-
| -> class C extends A { [ inherit-non-hot.scala:15 ]
14-
| -> val bAgain = toB.getBAgain [ inherit-non-hot.scala:16 ]
15-
| -> if b == null then b = new B(this) // error [ inherit-non-hot.scala:6 ]
16-
| -> def getBAgain: B = aCopy.toB [ inherit-non-hot.scala:12 ]
15+
| -> val c = new C [ inherit-non-hot.scala:19 ]
16+
| ^^^^^
17+
| -> class C extends A { [ inherit-non-hot.scala:15 ]
18+
| ^
19+
| -> val bAgain = toB.getBAgain [ inherit-non-hot.scala:16 ]
20+
| ^^^
21+
| -> if b == null then b = new B(this) // error [ inherit-non-hot.scala:6 ]
22+
| ^^^^^^^^^^^
23+
| -> def getBAgain: B = aCopy.toB [ inherit-non-hot.scala:12 ]
24+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

tests/init/neg/inlined-method.check

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
8 | scala.runtime.Scala3RunTime.assertFailed(message) // error
33
| ^^^^^^^
44
|Cannot prove that the value is fully initialized. Only initialized values may be used as arguments. Calling trace:
5-
| -> Assertion.failAssert(this) [ inlined-method.scala:2 ]
5+
|-> Assertion.failAssert(this) [ inlined-method.scala:2 ]
6+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^

tests/init/neg/local-warm4.check

+14-7
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@
22
18 | a = newA // error
33
| ^^^^
44
| Cannot prove that the value is fully initialized. May only assign fully initialized value. Calling trace:
5-
| -> val a = new A(5) [ local-warm4.scala:26 ]
6-
| -> class A(x: Int) extends Foo(x) { [ local-warm4.scala:6 ]
7-
| -> val b = new B(y) [ local-warm4.scala:10 ]
8-
| -> class B(x: Int) extends A(x) { [ local-warm4.scala:13 ]
9-
| -> class A(x: Int) extends Foo(x) { [ local-warm4.scala:6 ]
10-
| -> increment() [ local-warm4.scala:9 ]
11-
| -> updateA() [ local-warm4.scala:21 ]
5+
| -> val a = new A(5) [ local-warm4.scala:26 ]
6+
| ^^^^^^^^
7+
| -> class A(x: Int) extends Foo(x) { [ local-warm4.scala:6 ]
8+
| ^
9+
| -> val b = new B(y) [ local-warm4.scala:10 ]
10+
| ^^^^^^^^
11+
| -> class B(x: Int) extends A(x) { [ local-warm4.scala:13 ]
12+
| ^
13+
| -> class A(x: Int) extends Foo(x) { [ local-warm4.scala:6 ]
14+
| ^
15+
| -> increment() [ local-warm4.scala:9 ]
16+
| ^^^^^^^^^^^
17+
| -> updateA() [ local-warm4.scala:21 ]
18+
| ^^^^^^^^^

tests/init/neg/t3273.check

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
-- Error: tests/init/neg/t3273.scala:4:42 ------------------------------------------------------------------------------
22
4 | val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error
33
| ^^^^^^^^^^^^^^^
4-
| Cannot prove that the value is fully initialized. Only initialized values may be used as arguments.
4+
| Cannot prove that the value is fully initialized. Only initialized values may be used as arguments.
55
|
6-
| The unsafe promotion may cause the following problem:
7-
| Access non-initialized value num1. Calling trace:
6+
| The unsafe promotion may cause the following problem:
7+
| Access non-initialized value num1. Calling trace:
88
| -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error [ t3273.scala:4 ]
9+
| ^^^^
910
-- Error: tests/init/neg/t3273.scala:5:61 ------------------------------------------------------------------------------
1011
5 | val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error
1112
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12-
| Cannot prove that the value is fully initialized. Only initialized values may be used as arguments.
13+
| Cannot prove that the value is fully initialized. Only initialized values may be used as arguments.
1314
|
14-
| The unsafe promotion may cause the following problem:
15-
| Access non-initialized value num2. Calling trace:
15+
| The unsafe promotion may cause the following problem:
16+
| Access non-initialized value num2. Calling trace:
1617
| -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error [ t3273.scala:5 ]
18+
| ^^^^

tests/init/neg/unsound2.check

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
-- Error: tests/init/neg/unsound2.scala:5:26 ---------------------------------------------------------------------------
22
5 | def getN: Int = a.n // error
33
| ^^^
4-
| Access field B.this.a.n on a value with an unknown initialization status. Calling trace:
5-
| -> println(foo(x).getB) [ unsound2.scala:8 ]
4+
| Access field B.this.a.n on a value with an unknown initialization status. Calling trace:
5+
| -> println(foo(x).getB) [ unsound2.scala:8 ]
6+
| ^^^^^^
67
| -> def foo(y: Int): B = if (y > 10) then B(bar(y - 1), foo(y - 1).getN) else B(bar(y), 10) [ unsound2.scala:2 ]
8+
| ^^^^^^^^^^^^^^^

tests/init/neg/unsound3.check

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
10 | if (x < 12) then foo().getC().b else newB // error
33
| ^^^^^^^^^^^^^^
44
| Access field C.this.foo().getC().b on a value with an unknown initialization status. Calling trace:
5-
| -> val b = foo() [ unsound3.scala:12 ]
5+
| -> val b = foo() [ unsound3.scala:12 ]
6+
| ^^^^^

tests/init/neg/unsound4.check

+4-2
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@
22
3 | val aAgain = foo(5) // error
33
| ^
44
| Access non-initialized value aAgain. Calling trace:
5-
| -> val aAgain = foo(5) // error [ unsound4.scala:3 ]
6-
| -> def foo(x: Int): A = if (x < 5) then this else foo(x - 1).aAgain [ unsound4.scala:2 ]
5+
| -> val aAgain = foo(5) // error [ unsound4.scala:3 ]
6+
| ^^^^^^
7+
| -> def foo(x: Int): A = if (x < 5) then this else foo(x - 1).aAgain [ unsound4.scala:2 ]
8+
| ^^^^^^^^^^^^^^^^^

0 commit comments

Comments
 (0)