From c8722e4a2798e7433baa0faa5081ade919210ea9 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 21 Apr 2024 18:21:16 +0200 Subject: [PATCH 1/2] CC: Give more info when context function parameters leak Previously we had: parameter `$contextual1` leaks into outer capture set of type parameter `T` of method `apply`. We now give info in what type the parameter appeared and who owns the method. It's still not great, but at least we see more info that could tell us about the context. --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 10 ++- .../captures/effect-swaps.check | 19 ++++++ .../captures/effect-swaps.scala | 68 +++++++++++++++++++ .../captures/leaking-iterators.check | 2 +- .../captures/usingLogFile.check | 6 +- 5 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 tests/neg-custom-args/captures/effect-swaps.check create mode 100644 tests/neg-custom-args/captures/effect-swaps.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 9b6217033ede..4f57b5e0ed7a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -20,7 +20,7 @@ import Recheck.* import scala.collection.mutable import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult} import StdNames.nme -import NameKinds.{DefaultGetterName, WildcardParamName} +import NameKinds.{DefaultGetterName, WildcardParamName, UniqueNameKind} import reporting.trace /** The capture checker */ @@ -1288,10 +1288,14 @@ class CheckCaptures extends Recheck, SymTransformer: val added = widened.filter(isAllowed(_)) capt.println(i"heal $ref in $cs by widening to $added") if !added.subCaptures(cs, frozen = false).isOK then - val location = if meth.exists then i" of $meth" else "" + val location = if meth.exists then i" of ${meth.showLocated}" else "" + val paramInfo = + if ref.paramName.info.kind.isInstanceOf[UniqueNameKind] + then i"${ref.paramName} from ${ref.binder}" + else i"${ref.paramName}" val debugSetInfo = if ctx.settings.YccDebug.value then i" $cs" else "" report.error( - i"local reference ${ref.paramName} leaks into outer capture set$debugSetInfo of type parameter $paramName$location", + i"local reference $paramInfo leaks into outer capture set$debugSetInfo of type parameter $paramName$location", tree.srcPos) else widened.elems.foreach(recur) diff --git a/tests/neg-custom-args/captures/effect-swaps.check b/tests/neg-custom-args/captures/effect-swaps.check new file mode 100644 index 000000000000..086dce7f1841 --- /dev/null +++ b/tests/neg-custom-args/captures/effect-swaps.check @@ -0,0 +1,19 @@ +-- Error: tests/neg-custom-args/captures/effect-swaps.scala:62:6 ------------------------------------------------------- +61 | Result: +62 | Future: // error, escaping label from Result + | ^ + |local reference contextual$1 from (using contextual$1: boundary.Label[box Result[box Future[box T^?]^{fr, contextual$1, contextual$1}, box E^?]^?]^): + | box Result[box Future[box T^?]^{fr, contextual$1, contextual$1}, box E^?]^? leaks into outer capture set of type parameter T of method apply in object boundary +63 | fr.await.ok + |-------------------------------------------------------------------------------------------------------------------- + |Inline stack trace + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + |This location contains code that was inlined from effect-swaps.scala:37 +37 | boundary: + | ^^^^^^^^ + -------------------------------------------------------------------------------------------------------------------- +-- Error: tests/neg-custom-args/captures/effect-swaps.scala:66:11 ------------------------------------------------------ +66 | Result.make: // error, escaping label from Result + | ^^^^^^^^^^^ + |local reference contextual$9 from (using contextual$9: boundary.Label[Result[box Future[box T^?]^{fr, contextual$9}, box E^?]]^): + | box Future[box T^?]^{fr, contextual$9} leaks into outer capture set of type parameter T of method make in object Result diff --git a/tests/neg-custom-args/captures/effect-swaps.scala b/tests/neg-custom-args/captures/effect-swaps.scala new file mode 100644 index 000000000000..e8e609411b15 --- /dev/null +++ b/tests/neg-custom-args/captures/effect-swaps.scala @@ -0,0 +1,68 @@ +import annotation.capability + +object boundary: + + @capability final class Label[-T] + + /** Abort current computation and instead return `value` as the value of + * the enclosing `boundary` call that created `label`. + */ + def break[T](value: T)(using label: Label[T]): Nothing = ??? + + def apply[T](body: Label[T] ?=> T): T = ??? +end boundary + +import boundary.{Label, break} + +class Async +class Future[+T]: + this: Future[T]^ => + def await(using Async^): T = ??? +object Future: + def apply[T](op: Async^ ?=> T)(using Async): Future[T]^{op} = ??? + +abstract class Result[+T, +E] +case class Ok[+T](value: T) extends Result[T, Nothing] +case class Err[+E](value: E) extends Result[Nothing, E] + +object Result: + extension [T, E](r: Result[T, E]) + + /** `_.ok` propagates Err to current Label */ + inline def ok(using Label[Result[Nothing, E]]): T = r match + case r: Ok[_] => r.value + case err => break(err.asInstanceOf[Err[E]]) + + transparent inline def apply[T, E](inline body: Label[Result[T, E]] ?=> T): Result[T, E] = + boundary: + val result = body + Ok(result) + + // same as apply, but not an inline method + def make[T, E](body: Label[Result[T, E]] ?=> T): Result[T, E] = + boundary: + val result = body + Ok(result) + +end Result + +def test[T, E](using Async) = + val good1: List[Future[Result[T, E]]] => Future[Result[List[T], E]] = frs => + Future: + Result: + frs.map(_.await.ok) // OK + + val good2: Result[Future[T], E] => Future[Result[T, E]] = rf => + Future: + Result: + rf.ok.await // OK, Future argument has type Result[T] + + def fail3(fr: Future[Result[T, E]]^) = + Result: + Future: // error, escaping label from Result + fr.await.ok + + def fail4(fr: Future[Result[T, E]]^) = + Result.make: // error, escaping label from Result + Future: + fr.await.ok diff --git a/tests/neg-custom-args/captures/leaking-iterators.check b/tests/neg-custom-args/captures/leaking-iterators.check index 0481a9a4d9e2..2f47a26e894a 100644 --- a/tests/neg-custom-args/captures/leaking-iterators.check +++ b/tests/neg-custom-args/captures/leaking-iterators.check @@ -1,4 +1,4 @@ -- Error: tests/neg-custom-args/captures/leaking-iterators.scala:56:2 -------------------------------------------------- 56 | usingLogFile: log => // error | ^^^^^^^^^^^^ - | local reference log leaks into outer capture set of type parameter R of method usingLogFile + | local reference log leaks into outer capture set of type parameter R of method usingLogFile in package cctest diff --git a/tests/neg-custom-args/captures/usingLogFile.check b/tests/neg-custom-args/captures/usingLogFile.check index ef0c5d1e77c9..bf5c1dc4f83a 100644 --- a/tests/neg-custom-args/captures/usingLogFile.check +++ b/tests/neg-custom-args/captures/usingLogFile.check @@ -1,12 +1,12 @@ -- Error: tests/neg-custom-args/captures/usingLogFile.scala:23:14 ------------------------------------------------------ 23 | val later = usingLogFile { f => () => f.write(0) } // error | ^^^^^^^^^^^^ - | local reference f leaks into outer capture set of type parameter T of method usingLogFile + | local reference f leaks into outer capture set of type parameter T of method usingLogFile in object Test2 -- Error: tests/neg-custom-args/captures/usingLogFile.scala:28:23 ------------------------------------------------------ 28 | private val later2 = usingLogFile { f => Cell(() => f.write(0)) } // error | ^^^^^^^^^^^^ - | local reference f leaks into outer capture set of type parameter T of method usingLogFile + | local reference f leaks into outer capture set of type parameter T of method usingLogFile in object Test2 -- Error: tests/neg-custom-args/captures/usingLogFile.scala:44:16 ------------------------------------------------------ 44 | val later = usingFile("out", f => (y: Int) => xs.foreach(x => f.write(x + y))) // error | ^^^^^^^^^ - | local reference f leaks into outer capture set of type parameter T of method usingFile + | local reference f leaks into outer capture set of type parameter T of method usingFile in object Test3 From 6e080775f9849b6d532940b2c972bfef6ba4f00b Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 23 Apr 2024 17:22:42 +0200 Subject: [PATCH 2/2] Update test to show where the difference lies --- .../captures/effect-swaps.check | 28 ++++---- .../captures/effect-swaps.scala | 66 ++++++++++--------- 2 files changed, 49 insertions(+), 45 deletions(-) diff --git a/tests/neg-custom-args/captures/effect-swaps.check b/tests/neg-custom-args/captures/effect-swaps.check index 086dce7f1841..bda3509645d1 100644 --- a/tests/neg-custom-args/captures/effect-swaps.check +++ b/tests/neg-custom-args/captures/effect-swaps.check @@ -1,19 +1,21 @@ --- Error: tests/neg-custom-args/captures/effect-swaps.scala:62:6 ------------------------------------------------------- -61 | Result: -62 | Future: // error, escaping label from Result - | ^ - |local reference contextual$1 from (using contextual$1: boundary.Label[box Result[box Future[box T^?]^{fr, contextual$1, contextual$1}, box E^?]^?]^): - | box Result[box Future[box T^?]^{fr, contextual$1, contextual$1}, box E^?]^? leaks into outer capture set of type parameter T of method apply in object boundary -63 | fr.await.ok +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps.scala:64:8 ---------------------------------- +63 | Result: +64 | Future: // error, escaping label from Result + | ^ + | Found: Result.Ok[box Future[box T^?]^{fr, contextual$1}] + | Required: Result[Future[T], Nothing] +65 | fr.await.ok |-------------------------------------------------------------------------------------------------------------------- |Inline stack trace |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |This location contains code that was inlined from effect-swaps.scala:37 -37 | boundary: - | ^^^^^^^^ + |This location contains code that was inlined from effect-swaps.scala:41 +41 | boundary(Ok(body)) + | ^^^^^^^^ -------------------------------------------------------------------------------------------------------------------- --- Error: tests/neg-custom-args/captures/effect-swaps.scala:66:11 ------------------------------------------------------ -66 | Result.make: // error, escaping label from Result - | ^^^^^^^^^^^ + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/effect-swaps.scala:68:15 ------------------------------------------------------ +68 | Result.make: //lbl ?=> // error, escaping label from Result + | ^^^^^^^^^^^ |local reference contextual$9 from (using contextual$9: boundary.Label[Result[box Future[box T^?]^{fr, contextual$9}, box E^?]]^): | box Future[box T^?]^{fr, contextual$9} leaks into outer capture set of type parameter T of method make in object Result diff --git a/tests/neg-custom-args/captures/effect-swaps.scala b/tests/neg-custom-args/captures/effect-swaps.scala index e8e609411b15..1d72077bb8da 100644 --- a/tests/neg-custom-args/captures/effect-swaps.scala +++ b/tests/neg-custom-args/captures/effect-swaps.scala @@ -14,55 +14,57 @@ end boundary import boundary.{Label, break} -class Async +@capability trait Async +object Async: + def blocking[T](body: Async ?=> T): T = ??? + class Future[+T]: this: Future[T]^ => - def await(using Async^): T = ??? + def await(using Async): T = ??? object Future: - def apply[T](op: Async^ ?=> T)(using Async): Future[T]^{op} = ??? + def apply[T](op: Async ?=> T)(using Async): Future[T]^{op} = ??? + +enum Result[+T, +E]: + case Ok[+T](value: T) extends Result[T, Nothing] + case Err[+E](error: E) extends Result[Nothing, E] -abstract class Result[+T, +E] -case class Ok[+T](value: T) extends Result[T, Nothing] -case class Err[+E](value: E) extends Result[Nothing, E] object Result: - extension [T, E](r: Result[T, E]) + extension [T, E](r: Result[T, E]^)(using Label[Err[E]]) /** `_.ok` propagates Err to current Label */ - inline def ok(using Label[Result[Nothing, E]]): T = r match - case r: Ok[_] => r.value - case err => break(err.asInstanceOf[Err[E]]) + def ok: T = r match + case Ok(value) => value + case Err(value) => break[Err[E]](Err(value)) transparent inline def apply[T, E](inline body: Label[Result[T, E]] ?=> T): Result[T, E] = - boundary: - val result = body - Ok(result) + boundary(Ok(body)) // same as apply, but not an inline method def make[T, E](body: Label[Result[T, E]] ?=> T): Result[T, E] = - boundary: - val result = body - Ok(result) + boundary(Ok(body)) end Result def test[T, E](using Async) = - val good1: List[Future[Result[T, E]]] => Future[Result[List[T], E]] = frs => - Future: - Result: - frs.map(_.await.ok) // OK + import Result.* + Async.blocking: async ?=> + val good1: List[Future[Result[T, E]]] => Future[Result[List[T], E]] = frs => + Future: + Result: + frs.map(_.await.ok) // OK - val good2: Result[Future[T], E] => Future[Result[T, E]] = rf => - Future: - Result: - rf.ok.await // OK, Future argument has type Result[T] + val good2: Result[Future[T], E] => Future[Result[T, E]] = rf => + Future: + Result: + rf.ok.await // OK, Future argument has type Result[T] - def fail3(fr: Future[Result[T, E]]^) = - Result: - Future: // error, escaping label from Result - fr.await.ok + def fail3(fr: Future[Result[T, E]]^) = + Result: + Future: // error, escaping label from Result + fr.await.ok - def fail4(fr: Future[Result[T, E]]^) = - Result.make: // error, escaping label from Result - Future: - fr.await.ok + def fail4[T, E](fr: Future[Result[T, E]]^) = + Result.make: //lbl ?=> // error, escaping label from Result + Future: fut ?=> + fr.await.ok