From 9d6388d9f967408bb908080a1f362c0651830e12 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 27 Oct 2025 15:01:09 +0100 Subject: [PATCH 1/6] New escape hatch: discardUses [Cherry-picked 3bda0570cd4605bdbd6310894d4329994365bfef] --- compiler/src/dotty/tools/dotc/cc/CCState.scala | 14 ++++++++++++++ .../src/dotty/tools/dotc/cc/CheckCaptures.scala | 5 ++++- .../src/dotty/tools/dotc/core/Definitions.scala | 1 + library/src/scala/caps/package.scala | 3 +++ tests/pos-custom-args/captures/discardUses.scala | 2 ++ 5 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 tests/pos-custom-args/captures/discardUses.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CCState.scala b/compiler/src/dotty/tools/dotc/cc/CCState.scala index 061810c3cd42..9d63be99842a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CCState.scala +++ b/compiler/src/dotty/tools/dotc/cc/CCState.scala @@ -87,6 +87,8 @@ class CCState: private var collapseFresh: Boolean = false + private var discardUses: Boolean = false + object CCState: /** If we are currently in capture checking or setup, and `mt` is a method @@ -137,4 +139,16 @@ object CCState: /** Should all FreshCap instances be treated as equal to GlobalCap? */ def collapseFresh(using Context): Boolean = ccState.collapseFresh + /** Run `op` but suppress all recording of uses in `markFree` */ + inline def withDiscardedUses[T](op: => T)(using Context): T = + if isCaptureCheckingOrSetup then + val ccs = ccState + val saved = ccs.discardUses + ccs.discardUses = true + try op finally ccs.discardUses = saved + else op + + /** Should uses not be recorded in markFree? */ + def discardUses(using Context): Boolean = ccState.discardUses + end CCState diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index cca9814c00b1..f1fa2f4aaf96 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -574,7 +574,7 @@ class CheckCaptures extends Recheck, SymTransformer: // Under deferredReaches, don't propagate out of methods inside terms. // The use set of these methods will be charged when that method is called. - if !cs.isAlwaysEmpty then + if !cs.isAlwaysEmpty && !CCState.discardUses then recur(cs, curEnv, null) if addUseInfo then useInfos += ((tree, cs, curEnv)) end markFree @@ -783,6 +783,9 @@ class CheckCaptures extends Recheck, SymTransformer: else argType0.widen.stripCapturing capt.println(i"rechecking unsafeAssumePure of $arg with $pt: $argType") super.recheckFinish(argType, tree, pt) + else if meth == defn.Caps_unsafeDiscardUses then + val arg :: Nil = tree.args: @unchecked + withDiscardedUses(recheck(arg, pt)) else val res = super.recheckApply(tree, pt) includeCallCaptures(meth, res, tree) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 6eca9ed4d4bc..9005986b37f1 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1020,6 +1020,7 @@ class Definitions { @tu lazy val CapsUnsafeModule: Symbol = requiredModule("scala.caps.unsafe") @tu lazy val Caps_unsafeAssumePure: Symbol = CapsUnsafeModule.requiredMethod("unsafeAssumePure") @tu lazy val Caps_unsafeAssumeSeparate: Symbol = CapsUnsafeModule.requiredMethod("unsafeAssumeSeparate") + @tu lazy val Caps_unsafeDiscardUses: Symbol = CapsUnsafeModule.requiredMethod("unsafeDiscardUses") @tu lazy val Caps_unsafeErasedValue: Symbol = CapsUnsafeModule.requiredMethod("unsafeErasedValue") @tu lazy val Caps_ContainsTrait: TypeSymbol = CapsModule.requiredType("Contains") @tu lazy val Caps_ContainsModule: Symbol = requiredModule("scala.caps.Contains") diff --git a/library/src/scala/caps/package.scala b/library/src/scala/caps/package.scala index ae10018eb247..0527c3e149d6 100644 --- a/library/src/scala/caps/package.scala +++ b/library/src/scala/caps/package.scala @@ -208,6 +208,9 @@ object unsafe: */ def unsafeAssumeSeparate(op: Any): op.type = op + /** A wrapper around code for which uses go unrecorded */ + def unsafeDiscardUses(op: Any): op.type = op + /** An unsafe variant of erasedValue that can be used as an escape hatch. Unlike the * user-accessible compiletime.erasedValue, this version is assumed * to be a pure expression, hence capability safe. But there is no proof diff --git a/tests/pos-custom-args/captures/discardUses.scala b/tests/pos-custom-args/captures/discardUses.scala new file mode 100644 index 000000000000..37a2b85bef63 --- /dev/null +++ b/tests/pos-custom-args/captures/discardUses.scala @@ -0,0 +1,2 @@ +def test(c: Object^) = + val x: () -> Unit = caps.unsafe.unsafeDiscardUses(() => println(c)) From c0e4701d2cab1e9342cbb49281908fd60d6d1eff Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 28 Oct 2025 14:05:53 +0100 Subject: [PATCH 2/6] Don't perform the apply rule optimization under boxing This was already the case for the select rule optimization in recheckSelection but was missing for the corresponding rule for applications. This caused one regression in the standard library, here: ``` private def eagerHeadConcatIterators[A](it: Iterator[collection.Iterable[A]^]^): LazyListIterable[A]^{it} = if !it.hasNext then Empty else eagerHeadPrependIterator (it.next().iterator) (eagerHeadConcatIterators(it)) ``` Previously the access to `it.next()` was considered to have type `it` by applying the apply rule incorrectly. It should be `it*`. This means we now have `it*` instead of `it` in the result type and we also have an illegal use of `it*` leaking outside `eagerHeadConcatIterators`. The second problem was fixed by adding an unsafe escape hatch `unsafeDiscardUses` that suppressed use recording. This leads to: ``` private def eagerHeadConcatIterators[A](it: Iterator[collection.Iterable[A]^]^): LazyListIterable[A]^{it*} = if !it.hasNext then Empty else eagerHeadPrependIterator (caps.unsafe.unsafeDiscardUses(it.next()).iterator) (eagerHeadConcatIterators(it)) ``` This also did not compile since it claimed that the `it @reachCapability` was not a legal element of a capture set. The root cause was that we forced some definitions already in parser, which can lead to confusion when compiling the standard library itself. We now refrain from doing that and build the references to these annotations as untyped trees all the way down. [Cherry-picked 7bf957d01ae96bc6fc74aab7574fe308193fb060] --- compiler/src/dotty/tools/dotc/ast/untpd.scala | 11 +++-- .../dotty/tools/dotc/cc/CheckCaptures.scala | 6 ++- .../src/dotty/tools/dotc/core/StdNames.scala | 3 ++ .../immutable/LazyListIterable.scala | 19 ++++++-- .../neg-custom-args/captures/apply-rule.check | 46 +++++++++++++++++++ .../neg-custom-args/captures/apply-rule.scala | 16 +++++++ tests/neg-custom-args/captures/nicolas1.check | 12 +++++ tests/neg-custom-args/captures/nicolas1.scala | 11 +++++ 8 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 tests/neg-custom-args/captures/apply-rule.check create mode 100644 tests/neg-custom-args/captures/apply-rule.scala create mode 100644 tests/neg-custom-args/captures/nicolas1.check create mode 100644 tests/neg-custom-args/captures/nicolas1.scala diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 17dbb5bff213..bad70cb3a01c 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -523,7 +523,10 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { def rootDot(name: Name)(implicit src: SourceFile): Select = Select(Ident(nme.ROOTPKG), name) def scalaDot(name: Name)(implicit src: SourceFile): Select = Select(rootDot(nme.scala), name) def scalaAnnotationDot(name: Name)(using SourceFile): Select = Select(scalaDot(nme.annotation), name) + def scalaAnnotationInternalDot(name: Name)(using SourceFile): Select = Select(scalaAnnotationDot(nme.internal), name) def scalaRuntimeDot(name: Name)(using SourceFile): Select = Select(scalaDot(nme.runtime), name) + def scalaCapsDot(name: Name)(using SourceFile): Select = Select(scalaDot(nme.caps), name) + def scalaCapsInternalDot(name: Name)(using SourceFile): Select = Select(scalaCapsDot(nme.internal), name) def scalaUnit(implicit src: SourceFile): Select = scalaDot(tpnme.Unit) def scalaAny(implicit src: SourceFile): Select = scalaDot(tpnme.Any) @@ -553,16 +556,16 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { Annotated(parent, annot) def makeReachAnnot()(using Context): Tree = - New(ref(defn.ReachCapabilityAnnot.typeRef), Nil :: Nil) + New(scalaAnnotationInternalDot(tpnme.reachCapability), Nil :: Nil) def makeReadOnlyAnnot()(using Context): Tree = - New(ref(defn.ReadOnlyCapabilityAnnot.typeRef), Nil :: Nil) + New(scalaAnnotationInternalDot(tpnme.readOnlyCapability), Nil :: Nil) def makeOnlyAnnot(qid: Tree)(using Context) = - New(AppliedTypeTree(ref(defn.OnlyCapabilityAnnot.typeRef), qid :: Nil), Nil :: Nil) + New(AppliedTypeTree(scalaAnnotationInternalDot(tpnme.onlyCapability), qid :: Nil), Nil :: Nil) def makeConsumeAnnot()(using Context): Tree = - New(ref(defn.ConsumeAnnot.typeRef), Nil :: Nil) + New(scalaCapsInternalDot(tpnme.consume), Nil :: Nil) def makeConstructor(tparams: List[TypeDef], vparamss: List[List[ValDef]], rhs: Tree = EmptyTree)(using Context): DefDef = DefDef(nme.CONSTRUCTOR, joinParams(tparams, vparamss), TypeTree(), rhs) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index f1fa2f4aaf96..9d17f66f1dd8 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -130,8 +130,8 @@ object CheckCaptures: |A classifier class is a class extending `caps.Capability` and directly extending `caps.Classifier`.""", ann.srcPos) check(ref) - case tpe => - report.error(em"$elem: $tpe is not a legal element of a capture set", ann.srcPos) + case elem => + report.error(em"$elem is not a legal element of a capture set", ann.srcPos) ann.retainedSet.retainedElementsRaw.foreach(check) /** Disallow bad roots anywhere in type `tp``. @@ -840,6 +840,8 @@ class CheckCaptures extends Recheck, SymTransformer: appType match case appType @ CapturingType(appType1, refs) if qualType.exists + && !qualType.isBoxedCapturing + && !resultType.isBoxedCapturing && !tree.fun.symbol.isConstructor && !resultType.captureSet.containsResultCapability && qualCaptures.mightSubcapture(refs) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 323c59a5711d..3213be389c9d 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -573,6 +573,7 @@ object StdNames { val ofDim: N = "ofDim" val on: N = "on" val only: N = "only" + val onlyCapability: N = "onlyCapability" val opaque: N = "opaque" val open: N = "open" val ordinal: N = "ordinal" @@ -591,6 +592,8 @@ object StdNames { val productPrefix: N = "productPrefix" val quotes : N = "quotes" val raw_ : N = "raw" + val reachCapability: N = "reachCapability" + val readOnlyCapability: N = "readOnlyCapability" val rd: N = "rd" val refl: N = "refl" val reflect: N = "reflect" diff --git a/library/src/scala/collection/immutable/LazyListIterable.scala b/library/src/scala/collection/immutable/LazyListIterable.scala index dd0ec12a14f6..88438f35d1ff 100644 --- a/library/src/scala/collection/immutable/LazyListIterable.scala +++ b/library/src/scala/collection/immutable/LazyListIterable.scala @@ -1264,13 +1264,26 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { if (it.hasNext) eagerCons(it.next(), newLL(eagerHeadFromIterator(it))) else Empty + // TODO This should be (xss: (collection.Iterable[A]^)*) override def concat[A](xss: collection.Iterable[A]*): LazyListIterable[A] = if (xss.knownSize == 0) empty else newLL(eagerHeadConcatIterators(xss.iterator)) - private def eagerHeadConcatIterators[A](it: Iterator[collection.Iterable[A]^]^): LazyListIterable[A]^{it} = - if (!it.hasNext) Empty - else eagerHeadPrependIterator(it.next().iterator)(eagerHeadConcatIterators(it)) + /* TODO This should be: + private def eagerHeadConcatIterators[A](it: Iterator[collection.Iterable[A]^]^): LazyListIterable[A]^{it*} = + if !it.hasNext then Empty + else + eagerHeadPrependIterator + (caps.unsafe.unsafeDiscardUses(it.next()).iterator) + (eagerHeadConcatIterators(it)) + */ + + private def eagerHeadConcatIterators[A](it: Iterator[collection.Iterable[A]]^): LazyListIterable[A]^{it} = + if !it.hasNext then Empty + else + eagerHeadPrependIterator + (it.next().iterator) + (eagerHeadConcatIterators(it)) /** An infinite LazyListIterable that repeatedly applies a given function to a start value. * diff --git a/tests/neg-custom-args/captures/apply-rule.check b/tests/neg-custom-args/captures/apply-rule.check new file mode 100644 index 000000000000..73665fc79fdf --- /dev/null +++ b/tests/neg-custom-args/captures/apply-rule.check @@ -0,0 +1,46 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/apply-rule.scala:3:19 ------------------------------------ +3 | val _: () -> A = x // error + | ^ + | Found: (x : () ->{s*} A^{}) + | Required: () -> A + | + | Note that capability s* is not included in capture set {}. + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/apply-rule.scala:5:19 ------------------------------------ +5 | val _: () -> A = y // error + | ^ + | Found: (y : () ->{s*} A^{}) + | Required: () -> A + | + | Note that capability s* is not included in capture set {}. + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/apply-rule.scala:9:19 ------------------------------------ +9 | val _: () -> A = x // error + | ^ + | Found: (x : () ->{C} A^{}) + | Required: () -> A + | + | Note that capability C is not included in capture set {}. + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/apply-rule.scala:11:19 ----------------------------------- +11 | val _: () -> A = y // error + | ^ + | Found: (y : () ->{C} A^{}) + | Required: () -> A + | + | Note that capability C is not included in capture set {}. + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/apply-rule.scala:2:11 --------------------------------------------------------- +2 | val x = s(0) // error + | ^^^^ + | Local reach capability s* leaks into capture scope of method select. + | You could try to abstract the capabilities referred to by s* in a capset variable. +-- Error: tests/neg-custom-args/captures/apply-rule.scala:4:12 --------------------------------------------------------- +4 | val y = s.head // error + | ^^^^^^ + | Local reach capability s* leaks into capture scope of method select. + | You could try to abstract the capabilities referred to by s* in a capset variable. diff --git a/tests/neg-custom-args/captures/apply-rule.scala b/tests/neg-custom-args/captures/apply-rule.scala new file mode 100644 index 000000000000..4a8ff1909dd6 --- /dev/null +++ b/tests/neg-custom-args/captures/apply-rule.scala @@ -0,0 +1,16 @@ +def select[A](s: Seq[() => A]) = + val x = s(0) // error + val _: () -> A = x // error + val y = s.head // error + val _: () -> A = y // error + +def select2[A, C^](s: Seq[() ->{C} A]) = + val x = s(0) + val _: () -> A = x // error + val y = s.head + val _: () -> A = y // error + + + + + diff --git a/tests/neg-custom-args/captures/nicolas1.check b/tests/neg-custom-args/captures/nicolas1.check new file mode 100644 index 000000000000..ec8700133439 --- /dev/null +++ b/tests/neg-custom-args/captures/nicolas1.check @@ -0,0 +1,12 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/nicolas1.scala:10:2 -------------------------------------- +10 | val all: Seq[Rand ?->{head, tail*} A] = head +: tail // error + | ^ + | Found: (contextual$1: Rand^'s1) ?->{head, tail*} A^'s2 + | Required: (Rand^) ?->{head} A + | + | Note that capability tail* is not included in capture set {head}. + | + | where: ^ refers to the universal root capability +11 | all(nextInt(all.length)) + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/nicolas1.scala b/tests/neg-custom-args/captures/nicolas1.scala new file mode 100644 index 000000000000..ff592e74a1ca --- /dev/null +++ b/tests/neg-custom-args/captures/nicolas1.scala @@ -0,0 +1,11 @@ +import caps.* + +trait Rand extends SharedCapability: + def range(min: Int, max: Int): Int + +def nextInt(max: Int): Rand ?-> Int = + r ?=> r.range(0, max) + +def oneOf[A](head: Rand ?=> A, tail: (Rand ?=> A)*): Rand ?->{head} A = + val all: Seq[Rand ?->{head, tail*} A] = head +: tail // error + all(nextInt(all.length)) \ No newline at end of file From 0d480029b2bd2ef30221324509aa4f326fba6560 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Tue, 9 Dec 2025 11:33:44 +0100 Subject: [PATCH 3/6] Work around https://github.com/sbt/sbt/issues/8376 and re-enable `shapeless-3` community build (#24709) - works around https://github.com/sbt/sbt/issues/8376 / https://github.com/coursier/coursier/issues/3520 - kudos to @eed3si9n for suggesting this, as per https://github.com/coursier/coursier/issues/3520#issuecomment-3630791062 - re-enables tests disabled in https://github.com/scala/scala3/pull/24477#discussion_r2583852505 [Cherry-picked 577721f395350461499c3a71a7a0c934aa09109c] --- .../test/scala/dotty/communitybuild/CommunityBuildTest.scala | 2 +- project/Build.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/community-build/test/scala/dotty/communitybuild/CommunityBuildTest.scala b/community-build/test/scala/dotty/communitybuild/CommunityBuildTest.scala index 1a7591794712..ae1217635662 100644 --- a/community-build/test/scala/dotty/communitybuild/CommunityBuildTest.scala +++ b/community-build/test/scala/dotty/communitybuild/CommunityBuildTest.scala @@ -93,7 +93,7 @@ class CommunityBuildTestC: @Test def scalaz = projects.scalaz.run() @Test def scas = projects.scas.run() @Test def sconfig = projects.sconfig.run() - //@Test def shapeless3 = projects.shapeless3.run() + @Test def shapeless3 = projects.shapeless3.run() @Test def sourcecode = projects.sourcecode.run() @Test def specs2 = projects.specs2.run() diff --git a/project/Build.scala b/project/Build.scala index 1b83180779aa..586a2e39f79f 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -3028,7 +3028,7 @@ object Build { publishConfiguration ~= (_.withOverwrite(true)), publishLocalConfiguration ~= (_.withOverwrite(true)), projectID ~= {id => - val line = "scala.versionLine" -> versionLine + val line = "info.scala.versionLine" -> versionLine id.withExtraAttributes(id.extraAttributes + line) }, Test / publishArtifact := false, From c8869a9c1c50a4513e675d6c64ce413f9e97663f Mon Sep 17 00:00:00 2001 From: Wojciech Mazur Date: Sun, 21 Dec 2025 19:13:56 +0100 Subject: [PATCH 4/6] Exclude `scala-reflect` from coursier dependencies - workaround issues with unresolved `org.scala-lang/scala-reflect/3.8.0-RC3-bin-SNAPSHOT` (no such problem on main) --- project/Build.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Build.scala b/project/Build.scala index 586a2e39f79f..8b4f1ea6c188 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1960,7 +1960,7 @@ object Build { "org.scala-lang.modules" % "scala-asm" % "9.9.0-scala-1", Dependencies.compilerInterface, "com.github.sbt" % "junit-interface" % "0.13.3" % Test, - ("io.get-coursier" %% "coursier" % "2.0.16" % Test).cross(CrossVersion.for3Use2_13), + ("io.get-coursier" %% "coursier" % "2.0.16" % Test).cross(CrossVersion.for3Use2_13).excludeAll(("org.scala-lang" % "scala-reflect")), ), // NOTE: The only difference here is that we drop `-Werror` and semanticDB for now Compile / scalacOptions := Seq("-deprecation", "-feature", "-unchecked", "-encoding", "UTF8", "-language:implicitConversions"), From 2b29ffb5b9144c0fb5b3865070898b85d9c26a61 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Tue, 9 Dec 2025 06:33:24 -0800 Subject: [PATCH 5/6] Vertical alignment in adaptOverloaded [Cherry-picked da1e073fae09d95e8b0232d0f32013da2444f943] --- .../src/dotty/tools/dotc/typer/Typer.scala | 89 +++++++++---------- 1 file changed, 41 insertions(+), 48 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 80fe71328467..f85c638998df 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -4306,54 +4306,47 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val alts = altDenots.map(altRef) resolveOverloaded(alts, pt) match - case alt :: Nil => - readaptSimplified(tree.withType(alt)) - case Nil => - // If no alternative matches, there are still two ways to recover: - // 1. If context is an application, try to insert an apply or implicit - // 2. If context is not an application, pick a alternative that does - // not take parameters. - - def errorNoMatch = errorTree(tree, NoMatchingOverload(altDenots, pt)) - - pt match - case pt: FunOrPolyProto if pt.applyKind != ApplyKind.Using => - // insert apply or convert qualifier, but only for a regular application - tryInsertApplyOrImplicit(tree, pt, locked)(errorNoMatch) - case _ => - tryParameterless(alts)(errorNoMatch) - - case ambiAlts => - // If there are ambiguous alternatives, and: - // 1. the types aren't erroneous - // 2. the expected type is not a function type - // 3. there exist a parameterless alternative - // - // Then, pick the parameterless alternative. - // See tests/pos/i10715-scala and tests/pos/i10715-java. - - /** Constructs an "ambiguous overload" error */ - def errorAmbiguous = - val remainingDenots = altDenots.filter(denot => ambiAlts.contains(altRef(denot))) - val addendum = - if ambiAlts.exists(!_.symbol.exists) then - i"""| - | - |Note: Overloaded definitions introduced by refinements cannot be resolved""" - else "" - errorTree(tree, AmbiguousOverload(tree, remainingDenots, pt, addendum)) - end errorAmbiguous - - if tree.tpe.isErroneous || pt.isErroneous then - tree.withType(UnspecifiedErrorType) - else - pt match - case _: FunProto => - errorAmbiguous - case _ => - tryParameterless(alts)(errorAmbiguous) - - end match + case alt :: Nil => + readaptSimplified(tree.withType(alt)) + case Nil => + // If no alternative matches, there are still two ways to recover: + // 1. If context is an application, try to insert an apply or implicit + // 2. If context is not an application, pick an alternative that does + // not take parameters. + def errorNoMatch = errorTree(tree, NoMatchingOverload(altDenots, pt)) + + pt match + case pt: FunOrPolyProto if pt.applyKind != ApplyKind.Using => + // insert apply or convert qualifier, but only for a regular application + tryInsertApplyOrImplicit(tree, pt, locked)(errorNoMatch) + case _ => + tryParameterless(alts)(errorNoMatch) + case ambiAlts => + // If there are ambiguous alternatives, and: + // 1. the types aren't erroneous + // 2. the expected type is not a function type + // 3. there exists a parameterless alternative + // + // Then, pick the parameterless alternative. See tests/pos/i10715-* + + /** Constructs an "ambiguous overload" error */ + def errorAmbiguous = + val remainingDenots = altDenots.filter(denot => ambiAlts.contains(altRef(denot))) + val addendum = + if ambiAlts.exists(!_.symbol.exists) then + i"""| + | + |Note: Overloaded definitions introduced by refinements cannot be resolved""" + else "" + errorTree(tree, AmbiguousOverload(tree, remainingDenots, pt, addendum)) + + pt match + case pt if tree.tpe.isErroneous || pt.isErroneous => + tree.withType(UnspecifiedErrorType) + case _: FunProto => + errorAmbiguous + case _ => + tryParameterless(alts)(errorAmbiguous) end adaptOverloaded def adaptToArgs(wtp: Type, pt: FunProto): Tree = wtp match { From f830c10e06eb94e1c2096c3ef9db9a92d7beda54 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Tue, 9 Dec 2025 09:00:52 -0800 Subject: [PATCH 6/6] Construct ref more correctly [Cherry-picked 14909dd61c751a1a550704efc74420b975beec86] --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 13 ++++++------- tests/pos/i24631/TestJava.java | 12 ++++++++++++ tests/pos/i24631/test.scala | 2 ++ tests/pos/i24631b.scala | 11 +++++++++++ 4 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 tests/pos/i24631/TestJava.java create mode 100644 tests/pos/i24631/test.scala create mode 100644 tests/pos/i24631b.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index f85c638998df..464fbec1750c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -4288,23 +4288,22 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val allDenots = ref.denot.alternatives if pt.isExtensionApplyProto then allDenots.filter(_.symbol.is(ExtensionMethod)) else allDenots + def altRef(alt: SingleDenotation) = TermRef(ref.prefix, ref.name, alt) + val alts = altDenots.map(altRef) typr.println(i"adapt overloaded $ref with alternatives ${altDenots map (_.info)}%\n\n %") /** Search for an alternative that does not take parameters. * If there is one, return it, otherwise return the error tree. */ - def tryParameterless(alts: List[TermRef])(error: => tpd.Tree): Tree = + def tryParameterless(error: => tpd.Tree): Tree = alts.filter(_.info.isParameterless) match case alt :: Nil => readaptSimplified(tree.withType(alt)) case _ => altDenots.find(_.info.paramInfoss == ListOfNil) match - case Some(alt) => readaptSimplified(tree.withType(alt.symbol.denot.termRef)) + case Some(alt) => readaptSimplified(tree.withType(altRef(alt))) case _ => error - def altRef(alt: SingleDenotation) = TermRef(ref.prefix, ref.name, alt) - val alts = altDenots.map(altRef) - resolveOverloaded(alts, pt) match case alt :: Nil => readaptSimplified(tree.withType(alt)) @@ -4320,7 +4319,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // insert apply or convert qualifier, but only for a regular application tryInsertApplyOrImplicit(tree, pt, locked)(errorNoMatch) case _ => - tryParameterless(alts)(errorNoMatch) + tryParameterless(errorNoMatch) case ambiAlts => // If there are ambiguous alternatives, and: // 1. the types aren't erroneous @@ -4346,7 +4345,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case _: FunProto => errorAmbiguous case _ => - tryParameterless(alts)(errorAmbiguous) + tryParameterless(errorAmbiguous) end adaptOverloaded def adaptToArgs(wtp: Type, pt: FunProto): Tree = wtp match { diff --git a/tests/pos/i24631/TestJava.java b/tests/pos/i24631/TestJava.java new file mode 100644 index 000000000000..1a869cbdd6db --- /dev/null +++ b/tests/pos/i24631/TestJava.java @@ -0,0 +1,12 @@ +package example; + +public abstract class TestJava { + public abstract T create(String foo); + + // Note that this is the method that's called from Scala code + public T create() { return create(""); } + + public static class Concrete extends TestJava { + @Override public String create(String foo) { return foo; } + } +} diff --git a/tests/pos/i24631/test.scala b/tests/pos/i24631/test.scala new file mode 100644 index 000000000000..c54eb7f98c69 --- /dev/null +++ b/tests/pos/i24631/test.scala @@ -0,0 +1,2 @@ +val s = new example.TestJava.Concrete().create +val s2: String = s diff --git a/tests/pos/i24631b.scala b/tests/pos/i24631b.scala new file mode 100644 index 000000000000..9e408729c6c3 --- /dev/null +++ b/tests/pos/i24631b.scala @@ -0,0 +1,11 @@ +//> using options -source:3.0-migration + +abstract class C[A]: + def create(s: String): A + def create(): A = create("") + +class D extends C[String]: + def create(s: String): String = s + +val s = D().create +val s2: String = s