Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
126 changes: 61 additions & 65 deletions compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala

Large diffs are not rendered by default.

92 changes: 60 additions & 32 deletions compiler/src/dotty/tools/dotc/reporting/Message.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import printing.Formatting.hl
import config.SourceVersion
import cc.CaptureSet
import cc.Capabilities.*
import util.Property

import scala.annotation.threadUnsafe

Expand Down Expand Up @@ -41,6 +42,11 @@ object Message:
i"\n$what can be rewritten automatically under -rewrite $optionStr."
else ""

/** A context property that turns off expansion of `^` or `=>` to explicit cap's.
* Used in the where clauses of error messages.
*/
val NoCapExpansion = new Property.Key[Unit]

/** A note can produce an added string for an error message */
abstract class Note:

Expand Down Expand Up @@ -103,9 +109,12 @@ object Message:
* If the entry was not yet recorded, allocate the next superscript corresponding
* to the same string in the same name space. The first recording is the string proper
* and following recordings get consecutive superscripts starting with 2.
* @param capOk if true and there is already a cap recorded that matches the entry,
* then use an expanded representation with explicit `cap` for the
* original string.
* @return The possibly superscripted version of `str`.
*/
def record(str: String, isType: Boolean, entry: Recorded)(using Context): String =
def record(str: String, isType: Boolean, entry: Recorded, capOK: Boolean = false)(using Context): String =
if disambi.recordOK(str) then
//println(s"recording $str, $isType, $entry")

Expand All @@ -119,9 +128,6 @@ object Message:
if (underlying.name == e1.name) underlying else e1.namedType.dealias.typeSymbol
case _ => e1
}
val key = SeenKey(str, isType)
val existing = seen(key)
lazy val dealiased = followAlias(entry)

/** All lambda parameters with the same name are given the same superscript as
* long as their corresponding binders have the same parameter name lists.
Expand All @@ -138,13 +144,34 @@ object Message:
case _ =>
false

// The length of alts corresponds to the number of superscripts we need to print.
var alts = existing.dropWhile(alt => !sameSuperscript(dealiased, followAlias(alt)))
if alts.isEmpty then
alts = entry :: existing
seen(key) = alts
/** The superscript in `existing` that matches the current entry, using
* `sameSuperscript` on dealiased entries as a test. The superscript is
* the index of the matching entry in existing, counting from the right
* and starting at 1. I.e last entry in the list has superscript 1, next to
* last entry has superscript 2, and so on. A result of 0 means that no
* matching entry exists.
*/
def matchingSuperscript(existing: List[Recorded]): Int =
lazy val dealiased = followAlias(entry)
existing.dropWhile(alt => !sameSuperscript(dealiased, followAlias(alt))).length

if capOK && ctx.property(NoCapExpansion).isEmpty
&& matchingSuperscript(seen(SeenKey("cap", isType = false))) > 0
then
val capStr = record("cap", isType = false, entry)
return str match
case "^" => s"^{$capStr}"
case "=>" => s"->{$capStr}"
case "?=>" => s"?->{$capStr}"

val key = SeenKey(str, isType)
val existing = seen(key)
var sup = matchingSuperscript(existing)
if sup == 0 then
seen(key) = entry :: existing
sup = existing.length + 1

val suffix = alts.length match {
val suffix = sup match {
case 1 => ""
case n => n.toString.toCharArray.map {
case '0' => '⁰'
Expand Down Expand Up @@ -184,26 +211,27 @@ object Message:
""
}

entry match
case param: TypeParamRef =>
s"is a type variable${addendum("constraint", TypeComparer.bounds(param))}"
case param: TermParamRef =>
s"is a reference to a value parameter"
case sym: Symbol =>
val info =
if (ctx.gadt.contains(sym))
sym.info & ctx.gadt.fullBounds(sym).nn
else
sym.info
s"is a ${ctx.printer.kindString(sym)}${sym.showExtendedLocation}${addendum("bounds", info)}"
case tp: SkolemType =>
s"is an unknown value of type ${tp.widen.show}"
case ref: RootCapability =>
val relation =
if keys.length > 1 then "refer to"
else if List("^", "=>", "?=>").exists(keys(0).startsWith) then "refers to"
else "is"
s"$relation ${ref.descr}"
inContext(ctx.withProperty(NoCapExpansion, Some(()))):
entry match
case param: TypeParamRef =>
s"is a type variable${addendum("constraint", TypeComparer.bounds(param))}"
case param: TermParamRef =>
s"is a reference to a value parameter"
case sym: Symbol =>
val info =
if (ctx.gadt.contains(sym))
sym.info & ctx.gadt.fullBounds(sym).nn
else
sym.info
s"is a ${ctx.printer.kindString(sym)}${sym.showExtendedLocation}${addendum("bounds", info)}"
case tp: SkolemType =>
s"is an unknown value of type ${tp.widen.show}"
case ref: RootCapability =>
val relation =
if keys.length > 1 then "refer to"
else if List("^", "=>", "?=>").exists(keys(0).startsWith) then "refers to"
else "is"
s"$relation ${ref.descr}"
end explanation

/** Produce a where clause with explanations for recorded iterms.
Expand Down Expand Up @@ -276,14 +304,14 @@ object Message:
if isUniversalCaptureSet(refs) && !defn.isFunctionType(parent) && !printDebug && seen.isActive =>
boxText
~ toTextLocal(parent)
~ seen.record("^", isType = true, refs.elems.nth(0).asInstanceOf[RootCapability])
~ seen.record("^", isType = true, refs.elems.nth(0).asInstanceOf[RootCapability], capOK = true)
case _ =>
super.toTextCapturing(parent, refs, boxText)

override def funMiddleText(isContextual: Boolean, isPure: Boolean, refs: GeneralCaptureSet | Null): Text =
refs match
case refs: CaptureSet if isUniversalCaptureSet(refs) && seen.isActive =>
seen.record(arrow(isContextual, isPure = false), isType = true, refs.elems.nth(0).asInstanceOf[RootCapability])
seen.record(arrow(isContextual, isPure = false), isType = true, refs.elems.nth(0).asInstanceOf[RootCapability], capOK = true)
case _ =>
super.funMiddleText(isContextual, isPure, refs)

Expand Down
26 changes: 22 additions & 4 deletions compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ trait MessageRendering {
import Highlight.*
import Offsets.*

/** The maximal number of lines of code that are shown in a message after the
* `^` and error message.
*/
private inline val maxRenderedLinesAfterPoint = 3

/** Remove ANSI coloring from `str`, useful for getting real length of
* strings
*
Expand Down Expand Up @@ -64,9 +69,21 @@ trait MessageRendering {
val lines = linesFrom(syntax)
val (before, after) = pos.beforeAndAfterPoint

def compress(offsetsAndLines: List[(Int, String)]): List[(Int, String)] =
if offsetsAndLines.isEmpty then offsetsAndLines
else
val compressedLines =
if offsetsAndLines.length > maxRenderedLinesAfterPoint then
offsetsAndLines.take(maxRenderedLinesAfterPoint - 2)
++ List(
(offsetsAndLines(maxRenderedLinesAfterPoint - 2)._1, "..."),
offsetsAndLines.last)
else offsetsAndLines
compressedLines

(
before.zip(lines).map(render),
after.zip(lines.drop(before.length)).map(render),
compress(after.zip(lines.drop(before.length))).map(render),
maxLen
)
}
Expand Down Expand Up @@ -140,7 +157,7 @@ trait MessageRendering {
*
* @return aligned error message
*/
private def errorMsg(pos: SourcePosition, msg: String)(using Context, Level, Offset): String = {
private def errorMsg(pos: SourcePosition, msg: String, addLine: Boolean)(using Context, Level, Offset): String = {
val padding = msg.linesIterator.foldLeft(pos.startColumnPadding) { (pad, line) =>
val lineLength = stripColor(line).length
val maxPad = math.max(0, ctx.settings.pageWidth.value - offset - lineLength) - offset
Expand All @@ -149,9 +166,10 @@ trait MessageRendering {
else pad
}

msg.linesIterator
val msgStr = msg.linesIterator
.map { line => offsetBox + (if line.isEmpty then "" else padding + line) }
.mkString(EOL)
if addLine then msgStr ++ s"${EOL}$offsetBox" else msgStr
}

// file.path or munge it to normalize for testing
Expand Down Expand Up @@ -280,7 +298,7 @@ trait MessageRendering {
if pos.exists && pos1.exists && pos1.source.file.exists then
val (srcBefore, srcAfter, offset) = sourceLines(pos1)
val marker = positionMarker(pos1)
val err = errorMsg(pos1, msg.message)
val err = errorMsg(pos1, msg.message, srcAfter.nonEmpty)
sb.append((srcBefore ::: marker :: err :: srcAfter).mkString(EOL))

if inlineStack.nonEmpty then
Expand Down
5 changes: 3 additions & 2 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1861,12 +1861,13 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
* the singleton list consisting of its position in `args`, otherwise `Nil`.
*/
def paramIndices(param: untpd.ValDef, args: List[untpd.Tree]): List[Int] = {
def loop(args: List[untpd.Tree], start: Int): List[Int] = args match {

def loop(args: List[untpd.Tree], start: Int): List[Int] = args match
case arg :: args1 =>
val others = loop(args1, start + 1)
if (refersTo(arg, param)) start :: others else others
case _ => Nil
}

val allIndices = loop(args, 0)
if (allIndices.length == 1) allIndices else Nil
}
Expand Down
1 change: 1 addition & 0 deletions tests/neg-custom-args/captures/boundary-homebrew.check
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
|where: ?=> refers to a fresh root capability created in anonymous function of type (using l1²: boundary.Label[boundary.Label[Int]^]^): boundary.Label[Int]^ when checking argument to parameter body of method apply
| ^ refers to the universal root capability
| cap is a fresh root capability created in anonymous function of type (using l2: boundary.Label[Int]^'s2): Int of parameter parameter l2² of method $anonfun
|
20 | boundary.break(l2)(using l1)
21 | 15
|
Expand Down
8 changes: 5 additions & 3 deletions tests/neg-custom-args/captures/boundary.check
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
| The leakage occurred when trying to match the following types:
|
| Found: scala.util.boundary.Label[Object^'s1]
| Required: scala.util.boundary.Label[Object^]^²
| Required: scala.util.boundary.Label[Object^{cap}]^
|
| where: ^ refers to a fresh root capability classified as Control in the type of value local
| cap is the universal root capability
|
| where: ^ and cap refer to the universal root capability
| ^² refers to a fresh root capability classified as Control in the type of value local
6 | boundary[Unit]: l2 ?=>
7 | boundary.break(l2)(using l1) // error
8 | ???
Expand Down Expand Up @@ -47,6 +48,7 @@
|
| where: ^ and cap² refer to the universal root capability
| ^² and cap refer to a fresh root capability created in package <empty>
|
6 | boundary[Unit]: l2 ?=>
7 | boundary.break(l2)(using l1) // error
8 | ???
Expand Down
6 changes: 3 additions & 3 deletions tests/neg-custom-args/captures/box-adapt-contra.check
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
-- Error: tests/neg-custom-args/captures/box-adapt-contra.scala:13:57 --------------------------------------------------
13 | val f1: (Cap^{c} => Unit) ->{c} Unit = useCap1[Cap^{c}](c) // error, was ok when cap was a root
| ^^^^^^^^^^^^^^^^^^^
| Cap^{c} => Unit cannot be box-converted to Cap^{c} ->{cap, c} Unit
| since the additional capture set {c} resulting from box conversion is not allowed in Cap^{c} => Unit
| Cap^{c} => Unit cannot be box-converted to Cap^{c} ->{cap, c} Unit
| since the additional capture set {c} resulting from box conversion is not allowed in Cap^{c} ->{cap} Unit
|
| where: => and cap refer to the universal root capability
| where: => and cap refer to the universal root capability
-- Error: tests/neg-custom-args/captures/box-adapt-contra.scala:19:54 --------------------------------------------------
19 | val f3: (Cap^{c} -> Unit) => Unit = useCap3[Cap^{c}](c) // error
| ^^^^^^^^^^^^^^^^^^^
Expand Down
1 change: 1 addition & 0 deletions tests/neg-custom-args/captures/capt1.check
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
| Required: A
|
| Note that capability x is not included in capture set {}.
|
28 | def m() = if x == null then y else y
|
| longer explanation available when compiling with `-explain`
Expand Down
2 changes: 2 additions & 0 deletions tests/neg-custom-args/captures/check-inferred.check
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
|
| Inferred type : () ->{Counter.this.count} Unit
| Externally visible type: () -> Unit
|
19 | count.put(count.get + 1)
-- Error: tests/neg-custom-args/captures/check-inferred.scala:20:13 ----------------------------------------------------
20 | val decr = () => // error
Expand All @@ -15,6 +16,7 @@
|
| Inferred type : () ->{Counter.this.count} Unit
| Externally visible type: () -> Unit
|
21 | count.put(count.get - 1)
-- Error: tests/neg-custom-args/captures/check-inferred.scala:24:14 ----------------------------------------------------
24 | val count = Ref(): Object^ // error // error
Expand Down
2 changes: 2 additions & 0 deletions tests/neg-custom-args/captures/class-caps.check
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
| Required: (Int, Int) -> Int
|
| Note that capability Test.this.console is not included in capture set {}.
|
19 | log(s"adding a ($a) to b ($b)")(using console)
20 | a + b
|
Expand All @@ -16,6 +17,7 @@
| Required: (Int, Int) -> Int
|
| Note that capability Test1.this.console is not included in capture set {}.
|
29 | log(s"adding a ($a) to b ($b)")(using console)
30 | a + b
|
Expand Down
3 changes: 3 additions & 0 deletions tests/neg-custom-args/captures/classifiers-secondclass.check
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
|Note that capability f.write is not included in capture set {cap.only[Read]}.
|
|where: cap is a fresh root capability created in anonymous function of type (f²: Levels.File^): Unit when checking argument to parameter op of method parReduce
|
42 | f.write(42) // the error stems from here
43 | a + b + f.read() // ok
|
Expand All @@ -20,6 +21,7 @@
|Note that capability f.write is not included in capture set {cap.only[Read]}.
|
|where: cap is a fresh root capability created in anonymous function of type (g²: Levels.File^): Unit when checking argument to parameter op of method parReduce
|
54 | f.write(42) // the error stems from here
55 | a + b + f.read() + g.read() // ok
|
Expand All @@ -33,6 +35,7 @@
|Note that capability g.write is not included in capture set {cap.only[Read]}.
|
|where: cap is a fresh root capability created in anonymous function of type (g²: Levels.File^): Unit when checking argument to parameter op of method parReduce
|
57 | g.write(42) // the error stems from here
58 | 0
|
Expand Down
2 changes: 2 additions & 0 deletions tests/neg-custom-args/captures/delayedRunops.check
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
| Required: () -> Unit
|
| Note that capability ops* is not included in capture set {}.
|
13 | val ops1 = ops
14 | runOps(ops1)
|
Expand All @@ -16,6 +17,7 @@
| Required: () -> Unit
|
| Note that capability ops* is not included in capture set {}.
|
25 | val ops1: List[() ->{ops*} Unit] = ops
26 | runOps(ops1)
|
Expand Down
3 changes: 3 additions & 0 deletions tests/neg-custom-args/captures/effect-swaps-explicit.check
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
| Required: Result[Future[T], Nothing]
|
| Note that capability fr is not included in capture set {}.
|
65 | fr.await.ok
|--------------------------------------------------------------------------------------------------------------------
|Inline stack trace
Expand All @@ -27,6 +28,7 @@
|
|where: ?=> refers to a fresh root capability created in method fail4 when checking argument to parameter body of method make
| ^ refers to the universal root capability
|
70 | fr.await.ok
|
| longer explanation available when compiling with `-explain`
Expand All @@ -40,6 +42,7 @@
|
|where: ?=> refers to a fresh root capability created in method fail5 when checking argument to parameter body of method make
| ^ refers to the universal root capability
|
74 | Future: fut ?=>
75 | fr.await.ok
|
Expand Down
Loading
Loading