From 9253f3b03e151c883679d9f236d4e366c9fe5b9d Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 29 Oct 2025 10:11:35 +0100 Subject: [PATCH 1/3] Add missing substitution when typing closure blocks Fix typing closure blocks where the expected result type refers to a closure parameter. A substitution was missing in this case. Fixes #23727 --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- tests/neg-custom-args/captures/filevar.check | 5 ++-- tests/neg-custom-args/captures/i15923.check | 24 ++++++++++++------- tests/pos-custom-args/captures/i23727.scala | 1 + 4 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 tests/pos-custom-args/captures/i23727.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 3e00907332f2..13686f197b00 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1044,7 +1044,7 @@ class CheckCaptures extends Recheck, SymTransformer: val localResType = pt match case RefinedType(_, _, mt: MethodType) => inContext(ctx.withOwner(anonfun)): - Internalize(mt)(resType) + Internalize(mt)(resType.substParams(mt, params.tpes)) case _ => resType mdef.tpt.updNuType(localResType) // Make sure we affect the info of the anonfun by the previous updNuType diff --git a/tests/neg-custom-args/captures/filevar.check b/tests/neg-custom-args/captures/filevar.check index 0bfc7f71cd9d..fd6e6c045598 100644 --- a/tests/neg-custom-args/captures/filevar.check +++ b/tests/neg-custom-args/captures/filevar.check @@ -4,9 +4,10 @@ |Found: (f: File^'s1) ->'s2 Unit |Required: (f: File^{l}) => Unit | - |Note that capability l cannot be included in outer capture set 's1 of parameter f. + |Note that capability l is not included in capture set {cap}. | - |where: => refers to a fresh root capability created in anonymous function of type (using l: scala.caps.Capability): File^{l} -> Unit when instantiating expected result type (f: File^{l}) ->{cap} Unit of function literal + |where: => refers to a fresh root capability created in anonymous function of type (using l²: scala.caps.Capability): File^{l²} -> Unit when instantiating expected result type (f: File^{l²}) ->{cap²} Unit of function literal + | cap is a fresh root capability in the type of variable file 16 | val o = Service() 17 | o.file = f 18 | o.log diff --git a/tests/neg-custom-args/captures/i15923.check b/tests/neg-custom-args/captures/i15923.check index e018a2ea9f4b..c355ed810864 100644 --- a/tests/neg-custom-args/captures/i15923.check +++ b/tests/neg-custom-args/captures/i15923.check @@ -1,22 +1,30 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15923.scala:27:23 --------------------------------------- 27 | val leak = withCap(cap => mkId(cap)) // error (was: no error here since type aliases don't box) | ^^^^^^^^^^^^^^^^ - |Found: (cap: test2.Cap^'s1) ->'s2 [T] => (op: test2.Cap^'s3 ->'s4 T) ->'s5 T - |Required: test2.Cap^{lcap} => [T] => (op: test2.Cap^'s6 ->'s7 T) ->'s8 T + |Found: (lcap: scala.caps.Capability^) ?->'s1 test2.Cap^{lcap} => [T] => (op: test2.Cap^{lcap} ->'s2 T) ->'s3 T + |Required: (lcap: scala.caps.Capability^) ?-> test2.Cap^{lcap} =>² [T] => (op: test2.Cap^{lcap²} ->'s2 T) ->'s3 T | - |Note that capability lcap cannot be included in outer capture set 's1 of parameter cap. + |Note that capability lcap cannot be included in outer capture set {lcap²}. | - |where: => refers to a fresh root capability created in anonymous function of type (using lcap: scala.caps.Capability): test2.Cap^{lcap} -> [T] => (op: test2.Cap^{lcap} => T) -> T when instantiating expected result type test2.Cap^{lcap} ->{cap²} [T] => (op: test2.Cap^'s6 ->'s7 T) ->'s8 T of function literal + |where: => refers to a root capability associated with the result type of (using lcap: scala.caps.Capability^): test2.Cap^{lcap} => [T] => (op: test2.Cap^{lcap} ->'s2 T) ->'s3 T + | =>² refers to a root capability associated with the result type of (using lcap: scala.caps.Capability^): test2.Cap^{lcap} =>² [T] => (op: test2.Cap^{lcap²} ->'s2 T) ->'s3 T + | ^ refers to the universal root capability + | lcap is a reference to a value parameter + | lcap² is a parameter in an anonymous function in method bar | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15923.scala:12:21 --------------------------------------- 12 | val leak = withCap(cap => mkId(cap)) // error | ^^^^^^^^^^^^^^^^ - |Found: (cap: Cap^'s9) ->'s10 Id[Cap^'s11]^'s12 - |Required: Cap^{lcap} => Id[Cap^'s13]^'s14 + |Found: (lcap: scala.caps.Capability^) ?->'s4 Cap^{lcap} => Id[Cap^{lcap}]^'s5 + |Required: (lcap: scala.caps.Capability^) ?-> Cap^{lcap} =>² Id[Cap^{lcap²}]^'s5 | - |Note that capability lcap cannot be included in outer capture set 's9 of parameter cap. + |Note that capability lcap² is not included in capture set {lcap}. | - |where: => refers to a fresh root capability created in anonymous function of type (using lcap: scala.caps.Capability): Cap^{lcap} -> Id[Cap] when instantiating expected result type Cap^{lcap} ->{cap²} Id[Cap^'s13]^'s14 of function literal + |where: => refers to a root capability associated with the result type of (using lcap: scala.caps.Capability^): Cap^{lcap} => Id[Cap^{lcap}]^'s5 + | =>² refers to a root capability associated with the result type of (using lcap: scala.caps.Capability^): Cap^{lcap} =>² Id[Cap^{lcap²}]^'s5 + | ^ refers to the universal root capability + | lcap is a reference to a value parameter + | lcap² is a parameter in an anonymous function in method bar | | longer explanation available when compiling with `-explain` diff --git a/tests/pos-custom-args/captures/i23727.scala b/tests/pos-custom-args/captures/i23727.scala new file mode 100644 index 000000000000..00174769a0f0 --- /dev/null +++ b/tests/pos-custom-args/captures/i23727.scala @@ -0,0 +1 @@ +object Test From ee22f549602de0466037a28a1d517842f3d31944 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 29 Oct 2025 11:01:57 +0100 Subject: [PATCH 2/3] Don't internalize if result type depends on lambda params This tends to give better error messages. --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 28 ++++++++++++------ tests/neg-custom-args/captures/filevar.check | 10 +++---- tests/neg-custom-args/captures/i15923.check | 29 ++++--------------- tests/neg-custom-args/captures/i15923.scala | 2 +- 4 files changed, 31 insertions(+), 38 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 13686f197b00..e7dfc74bd760 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1041,18 +1041,28 @@ class CheckCaptures extends Recheck, SymTransformer: checkConformsExpr(argType, paramType, param) .showing(i"compared expected closure formal $argType against $param with ${paramTpt.nuType}", capt) if resType.isValueType && isFullyDefined(resType, ForceDegree.none) then - val localResType = pt match + + def updateTpt(localResType: Type) = + mdef.tpt.updNuType(localResType) + // Make sure we affect the info of the anonfun by the previous updNuType + // unless the info is already defined in a previous phase and does not change. + assert(!anonfun.isCompleted || anonfun.denot.validFor.firstPhaseId != thisPhase.id) + + pt match case RefinedType(_, _, mt: MethodType) => - inContext(ctx.withOwner(anonfun)): - Internalize(mt)(resType.substParams(mt, params.tpes)) - case _ => resType - mdef.tpt.updNuType(localResType) - // Make sure we affect the info of the anonfun by the previous updNuType - // unless the info is already defined in a previous phase and does not change. - assert(!anonfun.isCompleted || anonfun.denot.validFor.firstPhaseId != thisPhase.id) - //println(i"updating ${mdef.tpt} to $localResType/${mdef.tpt.nuType}") + if !mt.isResultDependent then + // If mt is result dependent we could compensate this by + // internalizing `resType.substParams(mt, params.tpes)`. + // But this tends to give worse error messages, so we refrain + // from doing that and don't update the local result type instead. + val localResType = inContext(ctx.withOwner(anonfun)): + Internalize(mt)(resType) + updateTpt(localResType) + case _ => + updateTpt(resType) case _ => case Nil => + end matchParamsAndResult openClosures = (anonfun, pt) :: openClosures // openClosures is needed for errors but currently makes no difference diff --git a/tests/neg-custom-args/captures/filevar.check b/tests/neg-custom-args/captures/filevar.check index fd6e6c045598..41d749727a04 100644 --- a/tests/neg-custom-args/captures/filevar.check +++ b/tests/neg-custom-args/captures/filevar.check @@ -1,13 +1,13 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/filevar.scala:15:12 -------------------------------------- 15 | withFile: f => // error with level checking, was OK under both schemes before | ^ - |Found: (f: File^'s1) ->'s2 Unit - |Required: (f: File^{l}) => Unit + |Found: (l: scala.caps.Capability^) ?->'s1 File^'s2 ->'s3 Unit + |Required: (l: scala.caps.Capability^) ?-> (f: File^{l}) => Unit | - |Note that capability l is not included in capture set {cap}. + |Note that capability l cannot be included in outer capture set 's4 of parameter f. | - |where: => refers to a fresh root capability created in anonymous function of type (using l²: scala.caps.Capability): File^{l²} -> Unit when instantiating expected result type (f: File^{l²}) ->{cap²} Unit of function literal - | cap is a fresh root capability in the type of variable file + |where: => refers to a root capability associated with the result type of (using l: scala.caps.Capability^): (f: File^{l}) => Unit + | ^ refers to the universal root capability 16 | val o = Service() 17 | o.file = f 18 | o.log diff --git a/tests/neg-custom-args/captures/i15923.check b/tests/neg-custom-args/captures/i15923.check index c355ed810864..6ae4cb76f4ac 100644 --- a/tests/neg-custom-args/captures/i15923.check +++ b/tests/neg-custom-args/captures/i15923.check @@ -1,30 +1,13 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15923.scala:27:23 --------------------------------------- -27 | val leak = withCap(cap => mkId(cap)) // error (was: no error here since type aliases don't box) - | ^^^^^^^^^^^^^^^^ - |Found: (lcap: scala.caps.Capability^) ?->'s1 test2.Cap^{lcap} => [T] => (op: test2.Cap^{lcap} ->'s2 T) ->'s3 T - |Required: (lcap: scala.caps.Capability^) ?-> test2.Cap^{lcap} =>² [T] => (op: test2.Cap^{lcap²} ->'s2 T) ->'s3 T - | - |Note that capability lcap cannot be included in outer capture set {lcap²}. - | - |where: => refers to a root capability associated with the result type of (using lcap: scala.caps.Capability^): test2.Cap^{lcap} => [T] => (op: test2.Cap^{lcap} ->'s2 T) ->'s3 T - | =>² refers to a root capability associated with the result type of (using lcap: scala.caps.Capability^): test2.Cap^{lcap} =>² [T] => (op: test2.Cap^{lcap²} ->'s2 T) ->'s3 T - | ^ refers to the universal root capability - | lcap is a reference to a value parameter - | lcap² is a parameter in an anonymous function in method bar - | - | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15923.scala:12:21 --------------------------------------- 12 | val leak = withCap(cap => mkId(cap)) // error | ^^^^^^^^^^^^^^^^ - |Found: (lcap: scala.caps.Capability^) ?->'s4 Cap^{lcap} => Id[Cap^{lcap}]^'s5 - |Required: (lcap: scala.caps.Capability^) ?-> Cap^{lcap} =>² Id[Cap^{lcap²}]^'s5 + |Found: (lcap: scala.caps.Capability^) ?->'s1 Cap^'s2 ->'s3 Id[Cap^'s4]^'s5 + |Required: (lcap: scala.caps.Capability^) ?-> Cap^{lcap} => Id[Cap^'s6]^'s7 | - |Note that capability lcap² is not included in capture set {lcap}. + |Note that capability cap cannot be included in outer capture set 's6. | - |where: => refers to a root capability associated with the result type of (using lcap: scala.caps.Capability^): Cap^{lcap} => Id[Cap^{lcap}]^'s5 - | =>² refers to a root capability associated with the result type of (using lcap: scala.caps.Capability^): Cap^{lcap} =>² Id[Cap^{lcap²}]^'s5 - | ^ refers to the universal root capability - | lcap is a reference to a value parameter - | lcap² is a parameter in an anonymous function in method bar + |where: => refers to a root capability associated with the result type of (using lcap: scala.caps.Capability^): Cap^{lcap} => Id[Cap^'s6]^'s7 + | ^ refers to the universal root capability + | cap is a root capability associated with the result type of (x$0: Cap^'s2): Id[Cap^'s4]^'s5 | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i15923.scala b/tests/neg-custom-args/captures/i15923.scala index 9f7d176c6e14..8287be150761 100644 --- a/tests/neg-custom-args/captures/i15923.scala +++ b/tests/neg-custom-args/captures/i15923.scala @@ -24,6 +24,6 @@ object test2: result } - val leak = withCap(cap => mkId(cap)) // error (was: no error here since type aliases don't box) + val leak = withCap(cap => mkId(cap)) // no error here since type aliases don't box leak { cap => cap.use() } } \ No newline at end of file From 1209716b2b21dfbd822f07315ee6124bbe59db1c Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 29 Oct 2025 15:06:57 +0100 Subject: [PATCH 3/3] Fix test code --- tests/pos-custom-args/captures/i23727.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/pos-custom-args/captures/i23727.scala b/tests/pos-custom-args/captures/i23727.scala index 00174769a0f0..b4c584c7ec14 100644 --- a/tests/pos-custom-args/captures/i23727.scala +++ b/tests/pos-custom-args/captures/i23727.scala @@ -1 +1,3 @@ -object Test +def test1(): Unit = + val t1: (x: () => Unit) -> (y: () ->{x} Unit) -> Unit = + x => y => () // should ok, but error \ No newline at end of file