Skip to content

Commit fa03916

Browse files
authored
Better printing of capabilities in error messages (#23701)
- Special case in some situations so that we only print the name, not the underlying type. - Print TermParamRefs like other singleton types - Use unique names to print empty capture set variables
2 parents d240468 + cce5762 commit fa03916

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+299
-317
lines changed

compiler/src/dotty/tools/dotc/cc/Capability.scala

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -703,14 +703,6 @@ object Capabilities:
703703
(this eq y)
704704
|| this.match
705705
case x: FreshCap =>
706-
def levelOK =
707-
if ccConfig.useFreshLevels && !CCState.collapseFresh then
708-
val yOwner = y.levelOwner
709-
yOwner.isStaticOwner || x.ccOwner.isContainedIn(yOwner)
710-
else y.core match
711-
case ResultCap(_) | _: ParamRef => false
712-
case _ => true
713-
714706
vs.ifNotSeen(this)(x.hiddenSet.elems.exists(_.subsumes(y)))
715707
|| x.acceptsLevelOf(y)
716708
&& ( y.tryClassifyAs(x.hiddenSet.classifier)
@@ -788,6 +780,9 @@ object Capabilities:
788780
case _: Maybe => MaybeCapability(c1)
789781
case _ => c1
790782

783+
def showAsCapability(using Context) =
784+
i"capability ${ctx.printer.toTextCapability(this).show}"
785+
791786
def toText(printer: Printer): Text = printer.toTextCapability(this)
792787
end Capability
793788

compiler/src/dotty/tools/dotc/cc/CaptureSet.scala

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import CCState.*
2020
import TypeOps.AvoidMap
2121
import compiletime.uninitialized
2222
import Capabilities.*
23+
import Names.Name
24+
import NameKinds.CapsetName
2325

2426
/** A class for capture sets. Capture sets can be constants or variables.
2527
* Capture sets support inclusion constraints <:< where <:< is subcapturing.
@@ -738,6 +740,17 @@ object CaptureSet:
738740

739741
var description: String = ""
740742

743+
private var myRepr: Name | Null = null
744+
745+
/** A represtentation of this capture set as a unique name. We print
746+
* empty capture set variables in this representation. Bimapped sets have
747+
* the representation of their source set.
748+
*/
749+
def repr(using Context): Name = {
750+
if (myRepr == null) myRepr = CapsetName.fresh()
751+
myRepr.nn
752+
}
753+
741754
/** Check that all maps recorded in skippedMaps map `elem` to itself
742755
* or something subsumed by it.
743756
*/
@@ -1028,6 +1041,7 @@ object CaptureSet:
10281041
override def isMaybeSet: Boolean = bimap.isInstanceOf[MaybeMap]
10291042
override def toString = s"BiMapped$id($source, elems = $elems)"
10301043
override def summarize = bimap.getClass.toString
1044+
override def repr(using Context): Name = source.repr
10311045
end BiMapped
10321046

10331047
/** A variable with elements given at any time as { x <- source.elems | p(x) } */
@@ -1300,24 +1314,23 @@ object CaptureSet:
13001314
case cs: Var =>
13011315
if !cs.levelOK(elem) then
13021316
val levelStr = elem match
1303-
case ref: TermRef => i", defined in ${ref.symbol.maybeOwner}"
1304-
case _ => ""
1305-
i"""capability ${elem}$levelStr
1306-
|cannot be included in outer capture set $cs"""
1317+
case ref: TermRef => i", defined in ${ref.symbol.maybeOwner}\n"
1318+
case _ => " "
1319+
i"""${elem.showAsCapability}${levelStr}cannot be included in outer capture set $cs"""
13071320
else if !elem.tryClassifyAs(cs.classifier) then
1308-
i"""capability ${elem} is not classified as ${cs.classifier}, therefore it
1321+
i"""${elem.showAsCapability} is not classified as ${cs.classifier}, therefore it
13091322
|cannot be included in capture set $cs of ${cs.classifier.name} elements"""
13101323
else if cs.isBadRoot(elem) then
13111324
elem match
13121325
case elem: FreshCap =>
1313-
i"""local capability $elem created in ${elem.ccOwner}
1326+
i"""local ${elem.showAsCapability} created in ${elem.ccOwner}
13141327
|cannot be included in outer capture set $cs"""
13151328
case _ =>
1316-
i"universal capability $elem cannot be included in capture set $cs"
1329+
i"universal ${elem.showAsCapability} cannot be included in capture set $cs"
13171330
else
1318-
i"capability $elem cannot be included in capture set $cs"
1331+
i"${elem.showAsCapability} cannot be included in capture set $cs"
13191332
case _ =>
1320-
i"capability $elem is not included in capture set $cs$why"
1333+
i"${elem.showAsCapability} is not included in capture set $cs$why"
13211334

13221335
override def toText(printer: Printer): Text =
13231336
inContext(printer.printerContext):

compiler/src/dotty/tools/dotc/core/NameKinds.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ object NameKinds {
328328
val ExceptionBinderName: UniqueNameKind = new UniqueNameKind("ex")
329329
val ExistentialBinderName: UniqueNameKind = new UniqueNameKind("ex$")
330330
val SkolemName: UniqueNameKind = new UniqueNameKind("?")
331+
val CapsetName: UniqueNameKind = new UniqueNameKind("'s")
331332
val SuperArgName: UniqueNameKind = new UniqueNameKind("$superArg$")
332333
val DocArtifactName: UniqueNameKind = new UniqueNameKind("$doc")
333334
val UniqueInlineName: UniqueNameKind = new UniqueNameKind("$i")

compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,11 @@ class PlainPrinter(_ctx: Context) extends Printer {
173173
else if cs == CaptureSet.Fluid then "<fluid>"
174174
else
175175
val core: Text =
176-
if !cs.isConst && cs.elems.isEmpty then "?"
177-
else "{" ~ Text(cs.processElems(_.toList.map(toTextCapability)), ", ") ~ "}"
176+
if !cs.isConst && cs.elems.isEmpty then cs.asVar.repr.show
177+
else
178+
Str("'").provided(ccVerbose && !cs.isConst)
179+
~ "{" ~ Text(cs.processElems(_.toList.map(toTextCapability)), ", ") ~ "}"
178180
~ Str(".reader").provided(ccVerbose && cs.mutability == Mutability.Reader)
179-
~ Str("?").provided(ccVerbose && !cs.isConst)
180181
~ Str(s"#${cs.asVar.id}").provided(showUniqueIds && !cs.isConst)
181182
core ~ cs.optionalInfo
182183

@@ -243,8 +244,6 @@ class PlainPrinter(_ctx: Context) extends Printer {
243244
selectionString(tp)
244245
else
245246
toTextPrefixOf(tp) ~ selectionString(tp)
246-
case tp: TermParamRef =>
247-
ParamRefNameString(tp) ~ hashStr(tp.binder) ~ ".type"
248247
case tp: TypeParamRef =>
249248
val suffix =
250249
if showNestingLevel then

docs/_docs/reference/experimental/cc.md

Lines changed: 36 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,11 @@ followed by `^`. We'll see that this turns the parameter into a _capability_ who
4444
If we now try to define the problematic value `later`, we get a static error:
4545
```
4646
| val later = usingLogFile { f => () => f.write(0) }
47-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
48-
|The expression's type () => Unit is not allowed to capture the root capability `cap`.
49-
|This usually means that a capability persists longer than its allowed lifetime.
47+
| ^^^^^^^^^^^^^^^^^^^^^^^^^
48+
| Found: (f: java.io.FileOutputStream^'s1) ->'s2 () ->{f} Unit
49+
| Required: java.io.FileOutputStream^ => () ->'s3 Unit
50+
|
51+
| Note that capability f cannot be included in outer capture set 's3.
5052
```
5153
In this case, it was easy to see that the `logFile` capability escapes in the closure passed to `usingLogFile`. But capture checking also works for more complex cases.
5254
For instance, capture checking is able to distinguish between the following safe code:
@@ -58,11 +60,11 @@ val xs = usingLogFile { f =>
5860
and the following unsafe one:
5961
```scala
6062
val xs = usingLogFile { f =>
61-
LazyList(1, 2, 3).map { x => f.write(x); x * x }
63+
LzyList(1, 2, 3).map { x => f.write(x); x * x }
6264
}
6365
```
6466
An error would be issued in the second case, but not the first one (this assumes a capture-aware
65-
formulation of `LazyList` which we will present later in this page).
67+
formulation `LzyList` of lazily evaluated lists, which we will present later in this page).
6668

6769
It turns out that capture checking has very broad applications. Besides the various
6870
try-with-resources patterns, it can also be a key part to the solutions of many other long standing problems in programming languages. Among them:
@@ -87,12 +89,12 @@ The capture checker extension introduces a new kind of types and it enforces som
8789
Capture checking is done in terms of _capturing types_ of the form
8890
`T^{c₁, ..., cᵢ}`. Here `T` is a type, and `{c₁, ..., cᵢ}` is a _capture set_ consisting of references to capabilities `c₁, ..., cᵢ`.
8991

90-
A _capability_ is syntactically a method- or class-parameter, a local variable, or the `this` of an enclosing class. The type of a capability
92+
An _object capability_ is syntactically a method- or class-parameter, a local variable, or the `this` of an enclosing class. The type of a capability
9193
must be a capturing type with a non-empty capture set. We also say that
9294
variables that are capabilities are _tracked_.
9395

9496
In a sense, every
95-
capability gets its authority from some other, more sweeping capability which it captures. The most sweeping capability, from which ultimately all others are derived is written `cap`. We call it the _universal capability_.
97+
capability gets its authority from some other, more sweeping capability which it captures. The recursion stops with a _universal capability_, written `cap`, from which all other capabilities are ultimately derived.
9698
If `T` is a type, then `T^` is a shorthand for `T^{cap}`, meaning `T` can capture arbitrary capabilities.
9799

98100
Here is an example:
@@ -105,8 +107,8 @@ class Logger(fs: FileSystem^):
105107
def test(fs: FileSystem^) =
106108
val l: Logger^{fs} = Logger(fs)
107109
l.log("hello world!")
108-
val xs: LazyList[Int]^{l} =
109-
LazyList.from(1)
110+
val xs: LzyList[Int]^{l} =
111+
LzyList.from(1)
110112
.map { i =>
111113
l.log(s"computing elem # $i")
112114
i * i
@@ -118,9 +120,9 @@ and retained as a field in class `Logger`. Hence, the local variable `l` has typ
118120
`Logger^{fs}`: it is a `Logger` which retains the `fs` capability.
119121

120122
The second variable defined in `test` is `xs`, a lazy list that is obtained from
121-
`LazyList.from(1)` by logging and mapping consecutive numbers. Since the list is lazy,
123+
`LzyList.from(1)` by logging and mapping consecutive numbers. Since the list is lazy,
122124
it needs to retain the reference to the logger `l` for its computations. Hence, the
123-
type of the list is `LazyList[Int]^{l}`. On the other hand, since `xs` only logs but does
125+
type of the list is `LzyList[Int]^{l}`. On the other hand, since `xs` only logs but does
124126
not do other file operations, it retains the `fs` capability only indirectly. That's why
125127
`fs` does not show up in the capture set of `xs`.
126128

@@ -138,7 +140,7 @@ This type is a shorthand for `(A -> B)^{c, d}`, i.e. the function type `A -> B`
138140
The impure function type `A => B` is treated as an alias for `A ->{cap} B`. That is, impure functions are functions that can capture anything.
139141

140142
A capture annotation `^` binds more strongly than a function arrow. So
141-
`A -> B^{c}` is read as `A -> (B^{c})`.
143+
`A -> B^{c}` is read as `A -> (B^{c})` and `A -> B^` is read as `A -> (B^{cap})`.
142144

143145
Analogous conventions apply to context function types. `A ?=> B` is an impure context function, with `A ?-> B` as its pure complement.
144146

@@ -203,15 +205,15 @@ we have
203205
The set consisting of the root capability `{cap}` covers every other capture set. This is
204206
a consequence of the fact that, ultimately, every capability is created from `cap`.
205207

206-
**Example 2.** Consider again the FileSystem/Logger example from before. `LazyList[Int]` is a proper subtype of `LazyList[Int]^{l}`. So if the `test` method in that example
207-
was declared with a result type `LazyList[Int]`, we'd get a type error. Here is the error message:
208+
**Example 2.** Consider again the FileSystem/Logger example from before. `LzyList[Int]` is a proper subtype of `LzyList[Int]^{l}`. So if the `test` method in that example
209+
was declared with a result type `LzyList[Int]`, we'd get a type error. Here is the error message:
208210
```
209-
11 |def test(using fs: FileSystem^): LazyList[Int] = {
211+
11 |def test(using fs: FileSystem^): LzyList[Int] = {
210212
| ^
211-
| Found: LazyList[Int]^{fs}
212-
| Required: LazyList[Int]
213+
| Found: LzyList[Int]^{fs}
214+
| Required: LzyList[Int]
213215
```
214-
Why does it say `LazyList[Int]^{fs}` and not `LazyList[Int]^{l}`, which is, after all, the type of the returned value `xs`? The reason is that `l` is a local variable in the body of `test`, so it cannot be referred to in a type outside that body. What happens instead is that the type is _widened_ to the smallest supertype that does not mention `l`. Since `l` has capture set `fs`, we have that `{fs}` covers `{l}`, and `{fs}` is acceptable in a result type of `test`, so `{fs}` is the result of that widening.
216+
Why does it say `LzyList[Int]^{fs}` and not `LzyList[Int]^{l}`, which is, after all, the type of the returned value `xs`? The reason is that `l` is a local variable in the body of `test`, so it cannot be referred to in a type outside that body. What happens instead is that the type is _widened_ to the smallest supertype that does not mention `l`. Since `l` has capture set `fs`, we have that `{fs}` covers `{l}`, and `{fs}` is acceptable in a result type of `test`, so `{fs}` is the result of that widening.
215217
This widening is called _avoidance_; it is not specific to capture checking but applies to all variable references in Scala types.
216218

217219
## Capability Classes
@@ -361,39 +363,37 @@ This principle plays an important part in making capture checking concise and pr
361363

362364
## Escape Checking
363365

364-
Some capture sets are restricted so that
365-
they are not allowed to contain the universal capability.
366366

367-
Specifically, if a capturing type is an instance of a type variable, that capturing type
368-
is not allowed to carry the universal capability `cap`. There's a connection to tunnelling here.
369-
The capture set of a type has to be present in the environment when a type is instantiated from
370-
a type variable. But `cap` is not itself available as a global entity in the environment. Hence,
371-
an error should result.
367+
Capabilities follow the usual scoping discipline, which means that capture sets
368+
can contain only capabilities that are visible at the point where the set is defined.
372369

373-
We can now reconstruct how this principle produced the error in the introductory example, where
370+
We now reconstruct how this principle produced the error in the introductory example, where
374371
`usingLogFile` was declared like this:
375372
```scala
376373
def usingLogFile[T](op: FileOutputStream^ => T): T = ...
377374
```
378375
The error message was:
379376
```
380377
| val later = usingLogFile { f => () => f.write(0) }
381-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
382-
|The expression's type () => Unit is not allowed to capture the root capability `cap`.
383-
|This usually means that a capability persists longer than its allowed lifetime.
378+
| ^^^^^^^^^^^^^^^^^^^^^^^^^
379+
| Found: (f: java.io.FileOutputStream^'s1) ->'s2 () ->{f} Unit
380+
| Required: java.io.FileOutputStream^ => () ->'s3 Unit
381+
|
382+
| Note that capability f cannot be included in outer capture set 's3.
384383
```
385384
This error message was produced by the following logic:
386385

387386
- The `f` parameter has type `FileOutputStream^`, which makes it a capability.
388387
- Therefore, the type of the expression `() => f.write(0)` is `() ->{f} Unit`.
389388
- This makes the type of the whole closure passed to `usingLogFile` the dependent function type
390-
`(f: FileOutputStream^) -> () ->{f} Unit`.
389+
`(f: FileOutputStream^'s1) ->'s2 () ->{f} Unit`,
390+
for some as yet uncomputed capture sets `'s1` and `'s2`.
391391
- The expected type of the closure is a simple, parametric, impure function type `FileOutputStream^ => T`,
392392
for some instantiation of the type variable `T`.
393-
- The smallest supertype of the closure's dependent function type that is a parametric function type is
394-
`FileOutputStream^ => () ->{cap} Unit`
395-
- Hence, the type variable `T` is instantiated to `() ->{cap} Unit`, or abbreviated `() => Unit`,
396-
which causes the error.
393+
- Matching with the found type, `T` must have the shape `() ->'s3 Unit`, for
394+
some capture set `'s3` defined at the level of value `later`.
395+
- That capture set cannot include the capability `f` since `f` is locally bound.
396+
This causes the error.
397397

398398
An analogous restriction applies to the type of a mutable variable.
399399
Another way one could try to undermine capture checking would be to
@@ -406,23 +406,8 @@ usingLogFile { f =>
406406
}
407407
loophole()
408408
```
409-
But this will not compile either, since mutable variables cannot have universal capture sets.
410-
411-
One also needs to prevent returning or assigning a closure with a local capability in an argument of a parametric type. For instance, here is a
412-
slightly more refined attack:
413-
```scala
414-
class Cell[+A](x: A)
415-
val sneaky = usingLogFile { f => Cell(() => f.write(0)) }
416-
sneaky.x()
417-
```
418-
At the point where the `Cell` is created, the capture set of the argument is `f`, which
419-
is OK. But at the point of use, it is `cap` (because `f` is no longer in scope), which causes again an error:
420-
```
421-
| sneaky.x()
422-
| ^^^^^^^^
423-
|The expression's type () => Unit is not allowed to capture the root capability `cap`.
424-
|This usually means that a capability persists longer than its allowed lifetime.
425-
```
409+
But this will not compile either, since the capture set of the mutable variable `loophole` cannot refer to variable `f`, which is not visible
410+
where `loophole` is defined.
426411

427412
Looking at object graphs, we observe a monotonicity property: The capture set of an object `x` covers the capture sets of all objects reachable through `x`. This property is reflected in the type system by the following _monotonicity rule_:
428413

language-server/test/dotty/tools/languageserver/CompletionTest.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class CompletionTest {
2727

2828
@Test def completionFromNewScalaPredef: Unit = {
2929
code"class Foo { val foo = summ${m1} }"
30-
.completion(("summon", Method, "[T](using x: T): x.type"))
30+
.completion(("summon", Method, "[T](using x: T): (x : T)"))
3131
}
3232

3333
@Test def completionFromScalaPackage: Unit = {

tests/neg-custom-args/captures/box-adapt-cases.check

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
| ^^^^^^^^^^^^^^^^
44
| Found: (cap: Cap^{io}) ->{io} Int
55
| Required: Cap^{io} -> Int
6-
| Note that capability (io : Cap^) is not included in capture set {}.
6+
| Note that capability io is not included in capture set {}.
77
|
88
| longer explanation available when compiling with `-explain`
99
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:29:10 ------------------------------
1010
29 | x.value(cap => cap.use()) // error
1111
| ^^^^^^^^^^^^^^^^
12-
| Found: (cap: Cap^?) ->{io, fs} Int
12+
| Found: (cap: Cap^'s1) ->{io, fs} Int
1313
| Required: Cap^{io, fs} ->{io} Int
14-
| Note that capability (fs : Cap^) is not included in capture set {io}.
14+
| Note that capability fs is not included in capture set {io}.
1515
|
1616
| longer explanation available when compiling with `-explain`

tests/neg-custom-args/captures/byname.check

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,22 @@
1212
|
1313
|where: ?=> refers to a fresh root capability created in method test when checking argument to parameter ff of method h
1414
|
15-
|Note that capability (cap1 : Cap) is not included in capture set {cap2}.
15+
|Note that capability cap1 is not included in capture set {cap2}.
1616
|
1717
| longer explanation available when compiling with `-explain`
1818
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/byname.scala:19:5 ----------------------------------------
1919
19 | h(g()) // error
2020
| ^^^
21-
| Found: () ?->{cap2} I^?
21+
| Found: () ?->{cap2} I^'s1
2222
| Required: () ?->{cap1} I
23-
| Note that capability (cap2 : Cap) is not included in capture set {cap1}.
23+
| Note that capability cap2 is not included in capture set {cap1}.
2424
|
2525
| longer explanation available when compiling with `-explain`
2626
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/byname.scala:22:5 ----------------------------------------
2727
22 | h2(() => g())() // error
2828
| ^^^^^^^^^
29-
| Found: () ->{cap2} I^?
29+
| Found: () ->{cap2} I^'s2
3030
| Required: () ->{cap1} I
31-
| Note that capability (cap2 : Cap) is not included in capture set {cap1}.
31+
| Note that capability cap2 is not included in capture set {cap1}.
3232
|
3333
| longer explanation available when compiling with `-explain`

tests/neg-custom-args/captures/capt-depfun.check

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66
|
77
| where: => refers to a fresh root capability in the type of value dc
88
|
9-
| Note that capability (y : C^) is not included in capture set {}.
9+
| Note that capability y is not included in capture set {}.
1010
|
1111
| longer explanation available when compiling with `-explain`

0 commit comments

Comments
 (0)