diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000000..118a334b383a --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1 @@ +46a26945a172429740ebdd1fc83517130670080b diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0bb2943f581f..c8b5617fc534 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -692,7 +692,7 @@ jobs: - name: Publish Nightly if: "steps.not_yet_published.outcome == 'success'" run: | - ./project/scripts/sbtPublish ";project scala3-bootstrapped ;publishSigned ;sonatypeBundleRelease" + ./project/scripts/sbtPublish ";project scala3-bootstrapped ;publishSigned ;sonaRelease" nightly_documentation: runs-on: [self-hosted, Linux] @@ -862,7 +862,7 @@ jobs: scala3-${{ env.RELEASE_TAG }}.msi - name: Publish Release - run: ./project/scripts/sbtPublish ";project scala3-bootstrapped ;publishSigned ;sonatypeBundleUpload" + run: ./project/scripts/sbtPublish ";project scala3-bootstrapped ;publishSigned ;sonaUpload" open_issue_on_failure: diff --git a/.github/workflows/lts-backport.yaml b/.github/workflows/lts-backport.yaml index f8b930707c69..bf602cd075df 100644 --- a/.github/workflows/lts-backport.yaml +++ b/.github/workflows/lts-backport.yaml @@ -7,7 +7,8 @@ on: jobs: add-to-backporting-project: - if: "!contains(github.event.push.head_commit.message, '[Next only]')" + if: "!contains(github.event.push.head_commit.message, '[Next only]') && + github.repository == 'scala/scala3'" runs-on: ubuntu-latest steps: diff --git a/.github/workflows/publish-sdkman.yml b/.github/workflows/publish-sdkman.yml index c0f29db7f0b0..8b7cdc0dc234 100644 --- a/.github/workflows/publish-sdkman.yml +++ b/.github/workflows/publish-sdkman.yml @@ -24,7 +24,7 @@ on: CONSUMER-KEY: required: true CONSUMER-TOKEN: - required: true + required: true env: RELEASE-URL: 'https://github.com/scala/scala3/releases/download/${{ inputs.version }}' @@ -46,7 +46,7 @@ jobs: - platform: WINDOWS_64 archive : 'scala3-${{ inputs.version }}-x86_64-pc-win32.zip' steps: - - uses: sdkman/sdkman-release-action@a60691d56279724b4c9ff0399c0ae21d641ab75e + - uses: sdkman/sdkman-release-action@2800d4359ae097a99afea7e0370f0c6e726182a4 with: CONSUMER-KEY : ${{ secrets.CONSUMER-KEY }} CONSUMER-TOKEN : ${{ secrets.CONSUMER-TOKEN }} @@ -54,7 +54,7 @@ jobs: VERSION : ${{ inputs.version }} URL : '${{ env.RELEASE-URL }}/${{ matrix.archive }}' PLATFORM : ${{ matrix.platform }} - + default: runs-on: ubuntu-latest needs: publish diff --git a/.jvmopts b/.jvmopts index a50abf36aa42..4df4f826d1db 100644 --- a/.jvmopts +++ b/.jvmopts @@ -1,5 +1,5 @@ -Xss1m --Xms512m --Xmx4096m +-Xms1024m +-Xmx8192m -XX:MaxInlineLevel=35 -XX:ReservedCodeCacheSize=512m diff --git a/changelogs/3.7.1-RC1.md b/changelogs/3.7.1-RC1.md new file mode 100644 index 000000000000..8f61c0d2bf32 --- /dev/null +++ b/changelogs/3.7.1-RC1.md @@ -0,0 +1,157 @@ +# Highlights of the release + +- Support for JDK 25 [#23004](https://github.com/scala/scala3/pull/23004) +- Warn if interpolator uses toString [#20578](https://github.com/scala/scala3/pull/20578) +- Warn if match in block is not used for PartialFunction [#23002](https://github.com/scala/scala3/pull/23002) + +# Other changes and fixes + +## Annotations + +- Approximate annotated types in `wildApprox` [#22893](https://github.com/scala/scala3/pull/22893) +- Keep unused annot on params [#23037](https://github.com/scala/scala3/pull/23037) + +## Erasure + +- Disallow context function types as value-class parameters [#23015](https://github.com/scala/scala3/pull/23015) + +## Experimental: Capture Checking + +- Two fixes to handling of abstract types with cap bounds [#22838](https://github.com/scala/scala3/pull/22838) +- Drop idempotent type maps [#22910](https://github.com/scala/scala3/pull/22910) +- Fix setup of class constructors [#22980](https://github.com/scala/scala3/pull/22980) + +## Named Tuples + +- Call dealias after stripping type variables for tupleElementTypesUpTo [#23005](https://github.com/scala/scala3/pull/23005) +- Avoid loosing denotations of named types during `integrate` [#22839](https://github.com/scala/scala3/pull/22839) + +## Experimental: Unroll + +- Fix #22833: allow unroll annotation in methods of final class [#22926](https://github.com/scala/scala3/pull/22926) + +## Experimental: Referencable Package Objects + +- Add experimental.packageObjectValues language setting [#23001](https://github.com/scala/scala3/pull/23001) + +## Exports + +- Respect export alias for default arg forwarder [#21109](https://github.com/scala/scala3/pull/21109) + +## Extension Methods + +- Extension check checks for no parens not empty parens [#22825](https://github.com/scala/scala3/pull/22825) + +## GADTs + +- Fix: Prevent GADT reasoning in pattern alternatives [#22853](https://github.com/scala/scala3/pull/22853) + +## Linting + +- Dealias when looking into imports [#22889](https://github.com/scala/scala3/pull/22889) +- Process Export for unused check [#22984](https://github.com/scala/scala3/pull/22984) +- Drill into QuotePattern bindings symbol info [#22987](https://github.com/scala/scala3/pull/22987) +- No warn implicit param of overriding method [#22901](https://github.com/scala/scala3/pull/22901) +- No warn for evidence params of marker traits such as NotGiven [#22985](https://github.com/scala/scala3/pull/22985) + +## Initialization + +- Check for tasty error in template trees. [#22867](https://github.com/scala/scala3/pull/22867) + +## Metaprogramming: Compile-time + +- Fix issue with certain synthetics missing in compiletime.typechecks [#22978](https://github.com/scala/scala3/pull/22978) + +## Pattern Matching + +- Fix existing GADT constraints with introduced pattern-bound symbols [#22928](https://github.com/scala/scala3/pull/22928) + +## Pickling + +- Fix fromProduct synthesized code for parameter-dependent case classes [#22961](https://github.com/scala/scala3/pull/22961) + +## Presentation Compiler + +- Completions for requests just before string [#22894](https://github.com/scala/scala3/pull/22894) +- Fix: go to def should lead to all: apply, object and class [#22771](https://github.com/scala/scala3/pull/22771) +- Ignore ending `$` when looking at end marker names [#22798](https://github.com/scala/scala3/pull/22798) +- Feature: Skip auto importing symbols we know are wrong in current context [#22813](https://github.com/scala/scala3/pull/22813) +- Show the Autofill completion case as what would be auto-filled [#22819](https://github.com/scala/scala3/pull/22819) +- Bugfix: Fix issues with annotations not detected [#22878](https://github.com/scala/scala3/pull/22878) +- Improvement: Rework IndexedContext to reuse the previously calculated scopes [#22898](https://github.com/scala/scala3/pull/22898) +- Pc: Properly adjust indentation when inlining blocks [#22915](https://github.com/scala/scala3/pull/22915) +- Improvement: Support using directives in worksheets [#22957](https://github.com/scala/scala3/pull/22957) +- Fix: show hover for synthetics if explicitly used [#22973](https://github.com/scala/scala3/pull/22973) +- Pc: fix: inline value when def indentation equals 2 [#22990](https://github.com/scala/scala3/pull/22990) + +## Rewrites + +- Fix insertion of `using` in applications with trailing lambda syntax [#22937](https://github.com/scala/scala3/pull/22937) +- Test chars safely when highlighting [#22918](https://github.com/scala/scala3/pull/22918) + +## Reporting + +- Print infix operations in infix form [#22854](https://github.com/scala/scala3/pull/22854) + +## Scaladoc + +- Chore: add support for 'abstract override' modifier [#22802](https://github.com/scala/scala3/pull/22802) +- Scaladoc: fix generation of unique header ids [#22779](https://github.com/scala/scala3/pull/22779) + +## Typer + +- Disallow context bounds in type lambdas [#22659](https://github.com/scala/scala3/pull/22659) +- Refuse trailing type parameters in extractors [#22699](https://github.com/scala/scala3/pull/22699) +- Fix #22724: Revert the PolyType case in #21744 [#22820](https://github.com/scala/scala3/pull/22820) +- Fix isGenericArrayElement for higher-kinded types [#22938](https://github.com/scala/scala3/pull/22938) +- Tighten condition to preserve denotation in IntegrateMap [#23060](https://github.com/scala/scala3/pull/23060) + +## Transform + +- Mix in the `productPrefix` hash statically in case class `hashCode` [#22865](https://github.com/scala/scala3/pull/22865) + +## Value Classes + +- Fix #21918: Disallow value classes extending type aliases of AnyVal [#23021](https://github.com/scala/scala3/pull/23021) + + +# Contributors + +Thank you to all the contributors who made this release possible 🎉 + +According to `git shortlog -sn --no-merges 3.7.0..3.7.1-RC1` these are: + +``` + 135 Martin Odersky + 27 Som Snytt + 13 Matt Bovel + 10 Wojciech Mazur + 9 Hamza Remmal + 5 Quentin Bernet + 5 Tomasz Godzik + 4 aherlihy + 3 HarrisL2 + 3 Jan Chyb + 3 Natsu Kagami + 3 Ondrej Lhotak + 3 Sébastien Doeraene + 2 Piotr Chabelski + 2 Yichen Xu + 2 Yoonjae Jeon + 2 kasiaMarek + 1 Aleksey Troitskiy + 1 Daisy Li + 1 Dale Wijnand + 1 Jan-Pieter van den Heuvel + 1 Jędrzej Rochala + 1 Kacper Korban + 1 Katarzyna Marek + 1 Lukas Rytz + 1 Mikołaj Fornal + 1 Nikita Glushchenko + 1 Oliver Bračevac + 1 Ondřej Lhoták + 1 dependabot[bot] + 1 noti0na1 + 1 philippus +``` diff --git a/changelogs/3.7.1-RC2.md b/changelogs/3.7.1-RC2.md new file mode 100644 index 000000000000..6a9b9d88bb79 --- /dev/null +++ b/changelogs/3.7.1-RC2.md @@ -0,0 +1,25 @@ +# Backported chnages + +- Backport "chore: filter allowed source versions by import and by settings" to 3.7.1 (#23231) +- Backport "Bump Scala CLI to v1.8.0 (was v1.7.1)" to 3.7.1 (#23230) +- Backport "Mention extension in unused param warning" to 3.7.1 (#23229) +- Backport "Revert recent changes to opaque type proxy generation" to 3.7.1 (#23228) +- Backport "Remove premature caching of lookups for unused lint" to 3.7.1 (#23227) + +# Reverted changes + +- Revert "Make overload pruning based on result types less aggressive (#21744)" in 3.7.1-RC2 (#23239) + +# Contributors + +Thank you to all the contributors who made this release possible 🎉 + +According to `git shortlog -sn --no-merges 3.7.1-RC1..3.7.1-RC2` these are: + +``` + 4 Hamza Remmal + 4 Som Snytt + 3 Jan Chyb + 3 Wojciech Mazur + 1 Piotr Chabelski +``` diff --git a/changelogs/3.7.1.md b/changelogs/3.7.1.md new file mode 100644 index 000000000000..4ffb178aea89 --- /dev/null +++ b/changelogs/3.7.1.md @@ -0,0 +1,171 @@ +# Highlights of the release + +- Support for JDK 25 [#23004](https://github.com/scala/scala3/pull/23004) +- Warn if interpolator uses toString [#20578](https://github.com/scala/scala3/pull/20578) +- Warn if match in block is not used for PartialFunction [#23002](https://github.com/scala/scala3/pull/23002) + +# Other changes and fixes + +## Annotations + +- Approximate annotated types in `wildApprox` [#22893](https://github.com/scala/scala3/pull/22893) +- Keep unused annot on params [#23037](https://github.com/scala/scala3/pull/23037) + +## Erasure + +- Disallow context function types as value-class parameters [#23015](https://github.com/scala/scala3/pull/23015) + +## Experimental: Capture Checking + +- Two fixes to handling of abstract types with cap bounds [#22838](https://github.com/scala/scala3/pull/22838) +- Drop idempotent type maps [#22910](https://github.com/scala/scala3/pull/22910) +- Fix setup of class constructors [#22980](https://github.com/scala/scala3/pull/22980) + +## Experimental: Unroll + +- Fix #22833: allow unroll annotation in methods of final class [#22926](https://github.com/scala/scala3/pull/22926) + +## Experimental: Referencable Package Objects + +- Add experimental.packageObjectValues language setting [#23001](https://github.com/scala/scala3/pull/23001) + +## Exports + +- Respect export alias for default arg forwarder [#21109](https://github.com/scala/scala3/pull/21109) + +## Extension Methods + +- Extension check checks for no parens not empty parens [#22825](https://github.com/scala/scala3/pull/22825) + +## GADTs + +- Fix: Prevent GADT reasoning in pattern alternatives [#22853](https://github.com/scala/scala3/pull/22853) + +## Linting + +- Dealias when looking into imports [#22889](https://github.com/scala/scala3/pull/22889) +- Process Export for unused check [#22984](https://github.com/scala/scala3/pull/22984) +- Drill into QuotePattern bindings symbol info [#22987](https://github.com/scala/scala3/pull/22987) +- No warn implicit param of overriding method [#22901](https://github.com/scala/scala3/pull/22901) +- No warn for evidence params of marker traits such as NotGiven [#22985](https://github.com/scala/scala3/pull/22985) +- Mention extension in unused param warning [#23132](https://github.com/scala/scala3/pull/23132) +- Remove premature caching of lookups for unused lint [#22982](https://github.com/scala/scala3/pull/22982) +- Enclosing package p.q not visible as q [#23069](https://github.com/scala/scala3/pull/23069) + +## Inline + +- Revert recent changes to opaque type proxy generation [#23059](https://github.com/scala/scala3/pull/23059) + +## Initialization + +- Check for tasty error in template trees. [#22867](https://github.com/scala/scala3/pull/22867) + +## Metaprogramming: Compile-time + +- Fix issue with certain synthetics missing in compiletime.typechecks [#22978](https://github.com/scala/scala3/pull/22978) + +## Named Tuples + +- Call dealias after stripping type variables for tupleElementTypesUpTo [#23005](https://github.com/scala/scala3/pull/23005) +- Avoid loosing denotations of named types during `integrate` [#22839](https://github.com/scala/scala3/pull/22839) + +## Pattern Matching + +- Fix existing GADT constraints with introduced pattern-bound symbols [#22928](https://github.com/scala/scala3/pull/22928) + +## Pickling + +- Fix fromProduct synthesized code for parameter-dependent case classes [#22961](https://github.com/scala/scala3/pull/22961) + +## Presentation Compiler + +- Completions for requests just before string [#22894](https://github.com/scala/scala3/pull/22894) +- Fix: go to def should lead to all: apply, object and class [#22771](https://github.com/scala/scala3/pull/22771) +- Ignore ending `$` when looking at end marker names [#22798](https://github.com/scala/scala3/pull/22798) +- Feature: Skip auto importing symbols we know are wrong in current context [#22813](https://github.com/scala/scala3/pull/22813) +- Show the Autofill completion case as what would be auto-filled [#22819](https://github.com/scala/scala3/pull/22819) +- Bugfix: Fix issues with annotations not detected [#22878](https://github.com/scala/scala3/pull/22878) +- Improvement: Rework IndexedContext to reuse the previously calculated scopes [#22898](https://github.com/scala/scala3/pull/22898) +- Pc: Properly adjust indentation when inlining blocks [#22915](https://github.com/scala/scala3/pull/22915) +- Improvement: Support using directives in worksheets [#22957](https://github.com/scala/scala3/pull/22957) +- Fix: show hover for synthetics if explicitly used [#22973](https://github.com/scala/scala3/pull/22973) +- Pc: fix: inline value when def indentation equals 2 [#22990](https://github.com/scala/scala3/pull/22990) + +## Rewrites + +- Fix insertion of `using` in applications with trailing lambda syntax [#22937](https://github.com/scala/scala3/pull/22937) +- Test chars safely when highlighting [#22918](https://github.com/scala/scala3/pull/22918) + +## Reporting + +- Print infix operations in infix form [#22854](https://github.com/scala/scala3/pull/22854) + +## Runner + +- Bump Scala CLI to v1.8.0 (was v1.7.1) [#23168](https://github.com/scala/scala3/pull/23168) + +## Scaladoc + +- Chore: add support for 'abstract override' modifier [#22802](https://github.com/scala/scala3/pull/22802) +- Scaladoc: fix generation of unique header ids [#22779](https://github.com/scala/scala3/pull/22779) + +## Settings + +- Filter allowed source versions by import and by settings [#23215](https://github.com/scala/scala3/pull/23215) + +## Typer + +- Disallow context bounds in type lambdas [#22659](https://github.com/scala/scala3/pull/22659) +- Refuse trailing type parameters in extractors [#22699](https://github.com/scala/scala3/pull/22699) +- Fix #22724: Revert the PolyType case in #21744 [#22820](https://github.com/scala/scala3/pull/22820) +- Fix isGenericArrayElement for higher-kinded types [#22938](https://github.com/scala/scala3/pull/22938) +- Tighten condition to preserve denotation in IntegrateMap [#23060](https://github.com/scala/scala3/pull/23060) + +## Transform + +- Mix in the `productPrefix` hash statically in case class `hashCode` [#22865](https://github.com/scala/scala3/pull/22865) + +## Value Classes + +- Fix #21918: Disallow value classes extending type aliases of AnyVal [#23021](https://github.com/scala/scala3/pull/23021) + +# Contributors + +Thank you to all the contributors who made this release possible 🎉 + +According to `git shortlog -sn --no-merges 3.7.0..3.7.1` these are: + +``` + 135 Martin Odersky + 31 Som Snytt + 14 Wojciech Mazur + 13 Hamza Remmal + 13 Matt Bovel + 6 Jan Chyb + 5 Quentin Bernet + 5 Tomasz Godzik + 4 aherlihy + 3 HarrisL2 + 3 Natsu Kagami + 3 Ondrej Lhotak + 3 Piotr Chabelski + 3 Sébastien Doeraene + 2 Yichen Xu + 2 Yoonjae Jeon + 2 kasiaMarek + 1 Aleksey Troitskiy + 1 Daisy Li + 1 Dale Wijnand + 1 Jan-Pieter van den Heuvel + 1 Jędrzej Rochala + 1 Kacper Korban + 1 Katarzyna Marek + 1 Lukas Rytz + 1 Mikołaj Fornal + 1 Nikita Glushchenko + 1 Oliver Bračevac + 1 Ondřej Lhoták + 1 dependabot[bot] + 1 noti0na1 + 1 philippus +``` diff --git a/community-build/community-projects/cats b/community-build/community-projects/cats index 771c6c802f59..683f28dd0da4 160000 --- a/community-build/community-projects/cats +++ b/community-build/community-projects/cats @@ -1 +1 @@ -Subproject commit 771c6c802f59c72dbc1be1898081c9c882ddfeb0 +Subproject commit 683f28dd0da42e20c4bbf1515c7a7839c3d3c7a9 diff --git a/community-build/community-projects/scas b/community-build/community-projects/scas index acaad1055738..83d0f62bbc57 160000 --- a/community-build/community-projects/scas +++ b/community-build/community-projects/scas @@ -1 +1 @@ -Subproject commit acaad1055738dbbcae7b18e6c6c2fc95f06eb7d6 +Subproject commit 83d0f62bbc57691e509f07186b34847bafe4b96e diff --git a/community-build/community-projects/utest b/community-build/community-projects/utest index f4a9789e2750..f828696abf2f 160000 --- a/community-build/community-projects/utest +++ b/community-build/community-projects/utest @@ -1 +1 @@ -Subproject commit f4a9789e2750523feee4a3477efb42eb15424fc7 +Subproject commit f828696abf2fd554d37e8020fc5b4aaa2d143325 diff --git a/community-build/src/scala/dotty/communitybuild/CommunityBuildRunner.scala b/community-build/src/scala/dotty/communitybuild/CommunityBuildRunner.scala index b3065fefe87f..6aaaedb8a3dd 100644 --- a/community-build/src/scala/dotty/communitybuild/CommunityBuildRunner.scala +++ b/community-build/src/scala/dotty/communitybuild/CommunityBuildRunner.scala @@ -13,8 +13,7 @@ object CommunityBuildRunner: * is necessary since we run tests each time on a fresh * Docker container. We run the update on Docker container * creation time to create the cache of the dependencies - * and avoid network overhead. See https://github.com/lampepfl/dotty-drone - * for more infrastructural details. + * and avoid network overhead. */ extension (self: CommunityProject) def run()(using suite: CommunityBuildRunner): Unit = diff --git a/community-build/test/scala/dotty/communitybuild/CommunityBuildTest.scala b/community-build/test/scala/dotty/communitybuild/CommunityBuildTest.scala index 6181d4c3ddec..5c2ea408413c 100644 --- a/community-build/test/scala/dotty/communitybuild/CommunityBuildTest.scala +++ b/community-build/test/scala/dotty/communitybuild/CommunityBuildTest.scala @@ -95,7 +95,6 @@ class CommunityBuildTestC: @Test def sourcecode = projects.sourcecode.run() @Test def specs2 = projects.specs2.run() - // @Test def stdLib213 = projects.stdLib213.run() @Test def ujson = projects.ujson.run() @Test def upickle = projects.upickle.run() @Test def utest = projects.utest.run() diff --git a/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala b/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala index 97934935f352..817d0be54d26 100644 --- a/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala +++ b/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala @@ -285,7 +285,7 @@ class BTypesFromSymbols[I <: DottyBackendInterface](val int: I, val frontendAcce // tests/run/serialize.scala and https://github.com/typelevel/cats-effect/pull/2360). val privateFlag = !sym.isClass && (sym.is(Private) || (sym.isPrimaryConstructor && sym.owner.isTopLevelModuleClass)) - val finalFlag = sym.is(Final) && !toDenot(sym).isClassConstructor && !sym.is(Mutable, butNot = Accessor) && !sym.enclosingClass.is(Trait) + val finalFlag = sym.is(Final) && !toDenot(sym).isClassConstructor && !sym.isMutableVar && !sym.enclosingClass.is(Trait) import asm.Opcodes.* import GenBCodeOps.addFlagIf diff --git a/compiler/src/dotty/tools/backend/jvm/BackendUtils.scala b/compiler/src/dotty/tools/backend/jvm/BackendUtils.scala index ae423b6b80dd..601837b37c96 100644 --- a/compiler/src/dotty/tools/backend/jvm/BackendUtils.scala +++ b/compiler/src/dotty/tools/backend/jvm/BackendUtils.scala @@ -186,6 +186,7 @@ object BackendUtils { 21 -> asm.Opcodes.V21, 22 -> asm.Opcodes.V22, 23 -> asm.Opcodes.V23, - 24 -> asm.Opcodes.V24 + 24 -> asm.Opcodes.V24, + 25 -> asm.Opcodes.V25 ) } diff --git a/compiler/src/dotty/tools/dotc/CompilationUnit.scala b/compiler/src/dotty/tools/dotc/CompilationUnit.scala index 9c985ecd84b3..b627e149e5fb 100644 --- a/compiler/src/dotty/tools/dotc/CompilationUnit.scala +++ b/compiler/src/dotty/tools/dotc/CompilationUnit.scala @@ -158,10 +158,10 @@ object CompilationUnit { unit1 } - /** Create a compilation unit corresponding to an in-memory String. + /** Create a compilation unit corresponding to an in-memory String. * Used for `compiletime.testing.typeChecks`. */ - def apply(name: String, source: String)(using Context): CompilationUnit = { + def apply(name: String, source: String): CompilationUnit = { val src = SourceFile.virtual(name = name, content = source, maybeIncomplete = false) new CompilationUnit(src, null) } diff --git a/compiler/src/dotty/tools/dotc/Run.scala b/compiler/src/dotty/tools/dotc/Run.scala index d0fe07303e41..b4dbb76dc464 100644 --- a/compiler/src/dotty/tools/dotc/Run.scala +++ b/compiler/src/dotty/tools/dotc/Run.scala @@ -42,7 +42,8 @@ import dotty.tools.dotc.util.chaining.* import java.util.{Timer, TimerTask} /** A compiler run. Exports various methods to compile source files */ -class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with ConstraintRunInfo { +class Run(comp: Compiler, ictx: Context) +extends ImplicitRunInfo, ConstraintRunInfo, cc.CaptureRunInfo { /** Default timeout to stop looking for further implicit suggestions, in ms. * This is usually for the first import suggestion; subsequent suggestions @@ -519,6 +520,7 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint /** Print summary of warnings and errors encountered */ def printSummary(): Unit = { printMaxConstraint() + printMaxPath() val r = runContext.reporter if !r.errorsReported then profile.printSummary() @@ -529,6 +531,7 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint override def reset(): Unit = { super[ImplicitRunInfo].reset() super[ConstraintRunInfo].reset() + super[CaptureRunInfo].reset() myCtx = null myUnits = Nil myUnitsCached = Nil diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index c235143e97f1..2c30896105b2 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -703,7 +703,7 @@ object desugar { def isNonEnumCase = !isEnumCase && (isCaseClass || isCaseObject) val isValueClass = parents.nonEmpty && isAnyVal(parents.head) // This is not watertight, but `extends AnyVal` will be replaced by `inline` later. - val caseClassInScala2Library = isCaseClass && ctx.settings.YcompileScala2Library.value + val caseClassInScala2Library = isCaseClass && Feature.shouldBehaveAsScala2 val originalTparams = constr1.leadingTypeParams val originalVparamss = asTermOnly(constr1.trailingParamss) @@ -922,7 +922,7 @@ object desugar { val copyRestParamss = derivedVparamss.tail.nestedMap(vparam => cpy.ValDef(vparam)(rhs = EmptyTree)) var flags = Synthetic | constr1.mods.flags & copiedAccessFlags - if ctx.settings.YcompileScala2Library.value then flags &~= Private + if Feature.shouldBehaveAsScala2 then flags &~= Private DefDef( nme.copy, joinParams(derivedTparams, copyFirstParams :: copyRestParamss), @@ -983,7 +983,7 @@ object desugar { else { val appMods = var flags = Synthetic | constr1.mods.flags & copiedAccessFlags - if ctx.settings.YcompileScala2Library.value then flags &~= Private + if Feature.shouldBehaveAsScala2 then flags &~= Private Modifiers(flags).withPrivateWithin(constr1.mods.privateWithin) val appParamss = derivedVparamss.nestedZipWithConserve(constrVparamss)((ap, cp) => @@ -1066,7 +1066,7 @@ object desugar { paramss // drop leading () that got inserted by class // TODO: drop this once we do not silently insert empty class parameters anymore case paramss => paramss - val finalFlag = if ctx.settings.YcompileScala2Library.value then EmptyFlags else Final + val finalFlag = if Feature.shouldBehaveAsScala2 then EmptyFlags else Final // implicit wrapper is typechecked in same scope as constructor, so // we can reuse the constructor parameters; no derived params are needed. DefDef( @@ -2262,6 +2262,8 @@ object desugar { New(ref(defn.RepeatedAnnot.typeRef), Nil :: Nil)) else if op.name == nme.CC_REACH then Apply(ref(defn.Caps_reachCapability), t :: Nil) + else if op.name == nme.CC_READONLY then + Apply(ref(defn.Caps_readOnlyCapability), t :: Nil) else assert(ctx.mode.isExpr || ctx.reporter.errorsReported || ctx.mode.is(Mode.Interactive), ctx.mode) Select(t, op.name) diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index 32ab8378ae16..0f0165ce859e 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -9,6 +9,7 @@ import Annotations.Annotation import NameKinds.ContextBoundParamName import typer.ConstFold import reporting.trace +import config.Feature import Decorators.* import Constants.Constant @@ -466,7 +467,7 @@ trait UntypedTreeInfo extends TreeInfo[Untyped] { self: Trees.Instance[Untyped] */ private def defKind(tree: Tree)(using Context): FlagSet = unsplice(tree) match { case EmptyTree | _: Import => NoInitsInterface - case tree: TypeDef if ctx.settings.YcompileScala2Library.value => + case tree: TypeDef if Feature.shouldBehaveAsScala2 => if (tree.isClassDef) EmptyFlags else NoInitsInterface case tree: TypeDef => if (tree.isClassDef) NoInits else NoInitsInterface case tree: DefDef => @@ -479,7 +480,7 @@ trait UntypedTreeInfo extends TreeInfo[Untyped] { self: Trees.Instance[Untyped] NoInitsInterface else if tree.mods.is(Given) && tree.paramss.isEmpty then EmptyFlags // might become a lazy val: TODO: check whether we need to suppress NoInits once we have new lazy val impl - else if ctx.settings.YcompileScala2Library.value then + else if Feature.shouldBehaveAsScala2 then EmptyFlags else NoInits @@ -759,7 +760,7 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] => */ def isVariableOrGetter(tree: Tree)(using Context): Boolean = { def sym = tree.symbol - def isVar = sym.is(Mutable) + def isVar = sym.isMutableVarOrAccessor def isGetter = mayBeVarGetter(sym) && sym.owner.info.member(sym.name.asTermName.setterName).exists diff --git a/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala b/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala index 98d9a0ca85f6..414b27101b7d 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala @@ -56,7 +56,7 @@ class TreeTypeMap( /** Replace occurrences of `This(oldOwner)` in some prefix of a type * by the corresponding `This(newOwner)`. */ - private val mapOwnerThis = new TypeMap with cc.CaptureSet.IdempotentCaptRefMap { + private val mapOwnerThis = new TypeMap { private def mapPrefix(from: List[Symbol], to: List[Symbol], tp: Type): Type = from match { case Nil => tp case (cls: ClassSymbol) :: from1 => mapPrefix(from1, to.tail, tp.substThis(cls, to.head.thisType)) diff --git a/compiler/src/dotty/tools/dotc/ast/Trees.scala b/compiler/src/dotty/tools/dotc/ast/Trees.scala index fdefc14aadd6..fcc257da27e4 100644 --- a/compiler/src/dotty/tools/dotc/ast/Trees.scala +++ b/compiler/src/dotty/tools/dotc/ast/Trees.scala @@ -741,11 +741,11 @@ object Trees { } /** A tree representing a quote pattern `'{ type binding1; ...; body }` or `'[ type binding1; ...; body ]`. - * `QuotePattern`s are created the type checker when typing an `untpd.Quote` in a pattern context. + * `QuotePattern`s are created by the type checker when typing an `untpd.Quote` in a pattern context. * * `QuotePattern`s are checked are encoded into `unapply`s in the `staging` phase. * - * The `bindings` contain the list of quote pattern type variable definitions (`Bind`s) in the oreder in + * The `bindings` contain the list of quote pattern type variable definitions (`Bind`s) in the order in * which they are defined in the source. * * @param bindings Type variable definitions (`Bind` tree) diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index ae3ed9fcad3b..3432e349fdf6 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -842,14 +842,6 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { Closure(tree: Tree)(env, meth, tpt) } - // This is a more fault-tolerant copier that does not cause errors when - // function types in applications are undefined. - // This was called `Inliner.InlineCopier` before 3.6.3. - class ConservativeTreeCopier() extends TypedTreeCopier: - override def Apply(tree: Tree)(fun: Tree, args: List[Tree])(using Context): Apply = - if fun.tpe.widen.exists then super.Apply(tree)(fun, args) - else untpd.cpy.Apply(tree)(fun, args).withTypeUnchecked(tree.tpe) - override def skipTransform(tree: Tree)(using Context): Boolean = tree.tpe.isError implicit class TreeOps[ThisTree <: tpd.Tree](private val tree: ThisTree) extends AnyVal { diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 57c74d90b45d..145c61584fcc 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -206,6 +206,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case class Var()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Mutable) + case class Mut()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Mutable) + case class Implicit()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Implicit) case class Given()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Given) @@ -332,6 +334,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { def isEnumCase: Boolean = isEnum && is(Case) def isEnumClass: Boolean = isEnum && !is(Case) + def isMutableVar: Boolean = is(Mutable) && mods.exists(_.isInstanceOf[Mod.Var]) } @sharable val EmptyModifiers: Modifiers = Modifiers() diff --git a/compiler/src/dotty/tools/dotc/cc/CCState.scala b/compiler/src/dotty/tools/dotc/cc/CCState.scala new file mode 100644 index 000000000000..c92c9faa6fe6 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/CCState.scala @@ -0,0 +1,165 @@ +package dotty.tools +package dotc +package cc + +import core.* +import CaptureSet.{CompareResult, CompareFailure, VarState} +import collection.mutable +import reporting.Message +import Contexts.Context +import Types.MethodType +import Symbols.Symbol + +/** Capture checking state, which is known to other capture checking components */ +class CCState: + import CCState.* + + // ------ Error diagnostics ----------------------------- + + /** Error reprting notes produces since the last call to `test` */ + var notes: List[ErrorNote] = Nil + + def addNote(note: ErrorNote): Unit = + if !notes.exists(_.getClass == note.getClass) then + notes = note :: notes + + def test(op: => CompareResult): CompareResult = + val saved = notes + notes = Nil + try op match + case res: CompareFailure => res.withNotes(notes) + case res => res + finally notes = saved + + def testOK(op: => Boolean): CompareResult = + test(if op then CompareResult.OK else CompareResult.Fail(Nil)) + + /** Warnings relating to upper approximations of capture sets with + * existentially bound variables. + */ + val approxWarnings: mutable.ListBuffer[Message] = mutable.ListBuffer() + + // ------ Level handling --------------------------- + + private var curLevel: Level = outermostLevel + + /** The level of the current environment. Levels start at 0 and increase for + * each nested function or class. -1 means the level is undefined. + */ + def currentLevel(using Context): Level = curLevel + + /** Perform `op` in the next inner level */ + inline def inNestedLevel[T](inline op: T)(using Context): T = + val saved = curLevel + curLevel = curLevel.nextInner + try op finally curLevel = saved + + /** Perform `op` in the next inner level unless `p` holds. */ + inline def inNestedLevelUnless[T](inline p: Boolean)(inline op: T)(using Context): T = + val saved = curLevel + if !p then curLevel = curLevel.nextInner + try op finally curLevel = saved + + /** A map recording the level of a symbol */ + private val mySymLevel: mutable.Map[Symbol, Level] = mutable.Map() + + def symLevel(sym: Symbol): Level = mySymLevel.getOrElse(sym, undefinedLevel) + + def recordLevel(sym: Symbol)(using Context): Unit = mySymLevel(sym) = curLevel + + // ------ BiTypeMap adjustment ----------------------- + + private var myMapFutureElems = true + + /** When mapping a capture set with a BiTypeMap, should we create a BiMapped set + * so that future elements can also be mapped, and elements added to the BiMapped + * are back-propagated? Turned off when creating capture set variables for the + * first time, since we then do not want to change the binder to the original type + * without capture sets when back propagating. Error case where this shows: + * pos-customargs/captures/lists.scala, method m2c. + */ + def mapFutureElems(using Context) = myMapFutureElems + + /** Don't map future elements in this `op` */ + inline def withoutMappedFutureElems[T](op: => T)(using Context): T = + val saved = mapFutureElems + myMapFutureElems = false + try op finally myMapFutureElems = saved + + // ------ Iteration count of capture checking run + + private var iterCount = 1 + + def iterationId = iterCount + + def nextIteration[T](op: => T): T = + iterCount += 1 + try op finally iterCount -= 1 + + // ------ Global counters ----------------------- + + /** Next CaptureSet.Var id */ + var varId = 0 + + /** Next root id */ + var rootId = 0 + + // ------ VarState singleton objects ------------ + // See CaptureSet.VarState creation methods for documentation + + object Separate extends VarState.Separating + object HardSeparate extends VarState.Separating + object Unrecorded extends VarState.Unrecorded + object ClosedUnrecorded extends VarState.ClosedUnrecorded + + // ------ Context info accessed from companion object when isCaptureCheckingOrSetup is true + + private var openExistentialScopes: List[MethodType] = Nil + + private var capIsRoot: Boolean = false + +object CCState: + + opaque type Level = Int + + val undefinedLevel: Level = -1 + + val outermostLevel: Level = 0 + + extension (x: Level) + def isDefined: Boolean = x >= 0 + def <= (y: Level) = (x: Int) <= y + def nextInner: Level = if isDefined then x + 1 else x + + /** If we are currently in capture checking or setup, and `mt` is a method + * type that is not a prefix of a curried method, perform `op` assuming + * a fresh enclosing existential scope `mt`, otherwise perform `op` directly. + */ + inline def inNewExistentialScope[T](mt: MethodType)(op: => T)(using Context): T = + if isCaptureCheckingOrSetup then + val ccs = ccState + val saved = ccs.openExistentialScopes + if mt.marksExistentialScope then ccs.openExistentialScopes = mt :: ccs.openExistentialScopes + try op finally ccs.openExistentialScopes = saved + else + op + + /** The currently opened existential scopes */ + def openExistentialScopes(using Context): List[MethodType] = ccState.openExistentialScopes + + /** Run `op` under the assumption that `cap` can subsume all other capabilties + * except Result capabilities. Every use of this method should be scrutinized + * for whether it introduces an unsoundness hole. + */ + inline def withCapAsRoot[T](op: => T)(using Context): T = + if isCaptureCheckingOrSetup then + val ccs = ccState + val saved = ccs.capIsRoot + ccs.capIsRoot = true + try op finally ccs.capIsRoot = saved + else op + + /** Is `caps.cap` a root capability that is allowed to subsume other capabilities? */ + def capIsRoot(using Context): Boolean = ccState.capIsRoot + +end CCState diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala index f0018cc93d7e..2be492ed6189 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala @@ -42,7 +42,8 @@ case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean)(cls: Symbol) exte case cr: TermRef => ref(cr) case cr: TermParamRef => untpd.Ident(cr.paramName).withType(cr) case cr: ThisType => This(cr.cls) - // TODO: Will crash if the type is an annotated type, for example `cap?` + case root(_) => ref(root.cap) + // TODO: Will crash if the type is an annotated type, for example `cap.rd` } val arg = repeated(elems, TypeTree(defn.AnyType)) New(symbol.typeRef, arg :: Nil) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 92cd40a65d5a..afe0cfb6a8ff 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -7,47 +7,16 @@ import Types.*, Symbols.*, Contexts.*, Annotations.*, Flags.* import Names.TermName import ast.{tpd, untpd} import Decorators.*, NameOps.* -import config.SourceVersion import config.Printers.capt import util.Property.Key import tpd.* -import StdNames.nme -import config.Feature -import collection.mutable -import CCState.* -import reporting.Message +import CaptureSet.VarState +/** Attachment key for capturing type trees */ private val Captures: Key[CaptureSet] = Key() -object ccConfig: - - /** If true, allow mapping capture set variables under captureChecking with maps that are neither - * bijective nor idempotent. We currently do now know how to do this correctly in all - * cases, though. - */ - inline val allowUnsoundMaps = false - - /** If enabled, use a special path in recheckClosure for closures - * that are eta expansions. This can improve some error messages. - */ - inline val handleEtaExpansionsSpecially = true - - /** Don't require @use for reach capabilities that are accessed - * only in a nested closure. This is unsound without additional - * mitigation measures, as shown by unsound-reach-5.scala. - */ - inline val deferredReaches = false - - /** If true, use "sealed" as encapsulation mechanism, meaning that we - * check that type variable instantiations don't have `cap` in any of - * their capture sets. This is an alternative of the original restriction - * that `cap` can't be boxed or unboxed. It is dropped in 3.5 but used - * again in 3.6. - */ - def useSealed(using Context) = - Feature.sourceVersion.stable != SourceVersion.`3.5` - -end ccConfig +/** Context property to print root.Fresh(...) as "fresh" instead of "cap" */ +val PrintFresh: Key[Unit] = Key() /** Are we at checkCaptures phase? */ def isCaptureChecking(using Context): Boolean = @@ -72,59 +41,11 @@ def depFun(args: List[Type], resultType: Type, isContextual: Boolean, paramNames /** An exception thrown if a @retains argument is not syntactically a CaptureRef */ class IllegalCaptureRef(tpe: Type)(using Context) extends Exception(tpe.show) -/** Capture checking state, which is known to other capture checking components */ -class CCState: - - /** The last pair of capture reference and capture set where - * the reference could not be added to the set due to a level conflict. - */ - var levelError: Option[CaptureSet.CompareResult.LevelError] = None - - /** Warnings relating to upper approximations of capture sets with - * existentially bound variables. - */ - val approxWarnings: mutable.ListBuffer[Message] = mutable.ListBuffer() - - private var curLevel: Level = outermostLevel - private val symLevel: mutable.Map[Symbol, Int] = mutable.Map() - -object CCState: - - opaque type Level = Int - - val undefinedLevel: Level = -1 - - val outermostLevel: Level = 0 - - /** The level of the current environment. Levels start at 0 and increase for - * each nested function or class. -1 means the level is undefined. - */ - def currentLevel(using Context): Level = ccState.curLevel - - inline def inNestedLevel[T](inline op: T)(using Context): T = - val ccs = ccState - val saved = ccs.curLevel - ccs.curLevel = ccs.curLevel.nextInner - try op finally ccs.curLevel = saved - - inline def inNestedLevelUnless[T](inline p: Boolean)(inline op: T)(using Context): T = - val ccs = ccState - val saved = ccs.curLevel - if !p then ccs.curLevel = ccs.curLevel.nextInner - try op finally ccs.curLevel = saved - - extension (x: Level) - def isDefined: Boolean = x >= 0 - def <= (y: Level) = (x: Int) <= y - def nextInner: Level = if isDefined then x + 1 else x - - extension (sym: Symbol)(using Context) - def ccLevel: Level = ccState.symLevel.getOrElse(sym, -1) - def recordLevel() = ccState.symLevel(sym) = currentLevel -end CCState +/** A base trait for data producing addenda to error messages */ +trait ErrorNote /** The currently valid CCState */ -def ccState(using Context) = +def ccState(using Context): CCState = Phases.checkCapturesPhase.asInstanceOf[CheckCaptures].ccState1 extension (tree: Tree) @@ -136,6 +57,8 @@ extension (tree: Tree) def toCaptureRefs(using Context): List[CaptureRef] = tree match case ReachCapabilityApply(arg) => arg.toCaptureRefs.map(_.reach) + case ReadOnlyCapabilityApply(arg) => + arg.toCaptureRefs.map(_.readOnly) case CapsOfApply(arg) => arg.toCaptureRefs case _ => tree.tpe.dealiasKeepAnnots match @@ -165,7 +88,7 @@ extension (tree: Tree) elems case _ => if tree.symbol.maybeOwner == defn.RetainsCapAnnot - then ref(defn.captureRoot.termRef) :: Nil + then ref(root.cap) :: Nil else Nil extension (tp: Type) @@ -179,21 +102,22 @@ extension (tp: Type) * - annotated types that represent reach or maybe capabilities */ final def isTrackableRef(using Context): Boolean = tp match - case _: (ThisType | TermParamRef) => - true + case _: (ThisType | TermParamRef) => true case tp: TermRef => - ((tp.prefix eq NoPrefix) - || tp.symbol.isField && !tp.symbol.isStatic && tp.prefix.isTrackableRef - || tp.isRootCapability - ) && !tp.symbol.isOneOf(UnstableValueFlags) + !tp.underlying.exists // might happen during construction of lambdas with annotations on parameters + || + ((tp.prefix eq NoPrefix) + || tp.symbol.isField && !tp.symbol.isStatic && tp.prefix.isTrackableRef + || tp.isCap + ) && !tp.symbol.isOneOf(UnstableValueFlags) case tp: TypeRef => tp.symbol.isType && tp.derivesFrom(defn.Caps_CapSet) case tp: TypeParamRef => - tp.derivesFrom(defn.Caps_CapSet) + !tp.underlying.exists // might happen during construction of lambdas + || tp.derivesFrom(defn.Caps_CapSet) + case root.Result(_) => true case AnnotatedType(parent, annot) => - (annot.symbol == defn.ReachCapabilityAnnot - || annot.symbol == defn.MaybeCapabilityAnnot - ) && parent.isTrackableRef + defn.capabilityWrapperAnnots.contains(annot.symbol) && parent.isTrackableRef case _ => false @@ -222,6 +146,8 @@ extension (tp: Type) else tp match case tp @ ReachCapability(_) => tp.singletonCaptureSet + case ReadOnlyCapability(ref) => + ref.deepCaptureSet(includeTypevars).readOnly case tp: SingletonCaptureRef if tp.isTrackableRef => tp.reach.singletonCaptureSet case _ => @@ -239,7 +165,7 @@ extension (tp: Type) * the two capture sets are combined. */ def capturing(cs: CaptureSet)(using Context): Type = - if (cs.isAlwaysEmpty || cs.isConst && cs.subCaptures(tp.captureSet, frozen = true).isOK) + if (cs.isAlwaysEmpty || cs.isConst && cs.subCaptures(tp.captureSet, VarState.Separate).isOK) && !cs.keepAlways then tp else tp match @@ -259,8 +185,7 @@ extension (tp: Type) def boxed(using Context): Type = tp.dealias match case tp @ CapturingType(parent, refs) if !tp.isBoxed && !refs.isAlwaysEmpty => tp.annot match - case ann: CaptureAnnotation => - assert(!parent.derivesFrom(defn.Caps_CapSet)) + case ann: CaptureAnnotation if !parent.derivesFrom(defn.Caps_CapSet) => AnnotatedType(parent, ann.boxedAnnot) case ann => tp case tp: RealTypeBounds => @@ -268,9 +193,13 @@ extension (tp: Type) case _ => tp - /** The first element of this path type */ + /** The first element of this path type. Note that class parameter references + * are of the form this.C but their pathroot is still this.C, not this. + */ final def pathRoot(using Context): Type = tp.dealias match - case tp1: NamedType if tp1.symbol.owner.isClass => tp1.prefix.pathRoot + case tp1: NamedType + if tp1.symbol.maybeOwner.isClass && tp1.symbol != defn.captureRoot && !tp1.symbol.is(TypeParam) => + tp1.prefix.pathRoot case tp1 => tp1 /** If this part starts with `C.this`, the class `C`. @@ -306,18 +235,27 @@ extension (tp: Type) /** The capture set consisting of all top-level captures of `tp` that appear under a box. * Unlike for `boxed` this also considers parents of capture types, unions and * intersections, and type proxies other than abstract types. + * Furthermore, if the original type is a capture ref `x`, it replaces boxed universal sets + * on the fly with x*. */ def boxedCaptureSet(using Context): CaptureSet = - def getBoxed(tp: Type): CaptureSet = tp match + def getBoxed(tp: Type, pre: Type): CaptureSet = tp match case tp @ CapturingType(parent, refs) => - val pcs = getBoxed(parent) - if tp.isBoxed then refs ++ pcs else pcs + val pcs = getBoxed(parent, pre) + if !tp.isBoxed then + pcs + else if pre.exists && refs.containsRootCapability then + val reachRef = if refs.isReadOnly then pre.reach.readOnly else pre.reach + pcs ++ reachRef.singletonCaptureSet + else + pcs ++ refs + case ref: CaptureRef if ref.isTracked && !pre.exists => getBoxed(ref, ref) case tp: TypeRef if tp.symbol.isAbstractOrParamType => CaptureSet.empty - case tp: TypeProxy => getBoxed(tp.superType) - case tp: AndType => getBoxed(tp.tp1) ** getBoxed(tp.tp2) - case tp: OrType => getBoxed(tp.tp1) ++ getBoxed(tp.tp2) + case tp: TypeProxy => getBoxed(tp.superType, pre) + case tp: AndType => getBoxed(tp.tp1, pre) ** getBoxed(tp.tp2, pre) + case tp: OrType => getBoxed(tp.tp1, pre) ++ getBoxed(tp.tp2, pre) case _ => CaptureSet.empty - getBoxed(tp) + getBoxed(tp, NoType) /** Is the boxedCaptureSet of this type nonempty? */ def isBoxedCapturing(using Context): Boolean = @@ -345,7 +283,8 @@ extension (tp: Type) def forceBoxStatus(boxed: Boolean)(using Context): Type = tp.widenDealias match case tp @ CapturingType(parent, refs) if tp.isBoxed != boxed => val refs1 = tp match - case ref: CaptureRef if ref.isTracked || ref.isReach => ref.singletonCaptureSet + case ref: CaptureRef if ref.isTracked || ref.isReach || ref.isReadOnly => + ref.singletonCaptureSet case _ => refs CapturingType(parent, refs1, boxed) case _ => @@ -379,23 +318,58 @@ extension (tp: Type) case _ => false - /** Tests whether the type derives from `caps.Capability`, which means - * references of this type are maximal capabilities. + /** Is this a type extending `Mutable` that has update methods? */ + def isMutableType(using Context): Boolean = + tp.derivesFrom(defn.Caps_Mutable) + && tp.membersBasedOnFlags(Mutable | Method, EmptyFlags) + .exists(_.hasAltWith(_.symbol.isUpdateMethod)) + + /** Knowing that `tp` is a function type, is it an alias to a function other + * than `=>`? */ - def derivesFromCapability(using Context): Boolean = tp.dealias match + def isAliasFun(using Context): Boolean = tp match + case AppliedType(tycon, _) => !defn.isFunctionSymbol(tycon.typeSymbol) + case _ => false + + /** Tests whether all CapturingType parts of the type that are traversed for + * dcs computation satisfy at least one of two conditions: + * 1. They decorate classes that extend the given capability class `cls`, or + * 2. Their capture set is constant and consists only of capabilities + * the derive from `cls` in the sense of `derivesFromCapTrait`. + */ + def derivesFromCapTraitDeeply(cls: ClassSymbol)(using Context): Boolean = + val accumulate = new DeepTypeAccumulator[Boolean]: + def capturingCase(acc: Boolean, parent: Type, refs: CaptureSet) = + this(acc, parent) + && (parent.derivesFromCapTrait(cls) + || refs.isConst && refs.elems.forall(_.derivesFromCapTrait(cls))) + def abstractTypeCase(acc: Boolean, t: TypeRef, upperBound: Type) = + this(acc, upperBound) + accumulate(true, tp) + + /** Tests whether the type derives from capability class `cls`. */ + def derivesFromCapTrait(cls: ClassSymbol)(using Context): Boolean = tp.dealiasKeepAnnots match case tp: (TypeRef | AppliedType) => val sym = tp.typeSymbol - if sym.isClass then sym.derivesFrom(defn.Caps_Capability) - else tp.superType.derivesFromCapability + if sym.isClass then sym.derivesFrom(cls) + else tp.superType.derivesFromCapTrait(cls) + case ReachCapability(tp1) => + tp1.widen.derivesFromCapTraitDeeply(cls) + case ReadOnlyCapability(tp1) => + tp1.derivesFromCapTrait(cls) case tp: (TypeProxy & ValueType) => - tp.superType.derivesFromCapability + tp.superType.derivesFromCapTrait(cls) case tp: AndType => - tp.tp1.derivesFromCapability || tp.tp2.derivesFromCapability + tp.tp1.derivesFromCapTrait(cls) || tp.tp2.derivesFromCapTrait(cls) case tp: OrType => - tp.tp1.derivesFromCapability && tp.tp2.derivesFromCapability + tp.tp1.derivesFromCapTrait(cls) && tp.tp2.derivesFromCapTrait(cls) case _ => false + def derivesFromCapability(using Context): Boolean = derivesFromCapTrait(defn.Caps_Capability) + def derivesFromMutable(using Context): Boolean = derivesFromCapTrait(defn.Caps_Mutable) + def derivesFromSharedCapability(using Context): Boolean = derivesFromCapTrait(defn.Caps_SharedCapability) + /** Drop @retains annotations everywhere */ def dropAllRetains(using Context): Type = // TODO we should drop retains from inferred types before unpickling val tm = new TypeMap: @@ -406,17 +380,6 @@ extension (tp: Type) mapOver(t) tm(tp) - /** If `x` is a capture ref, its reach capability `x*`, represented internally - * as `x @reachCapability`. `x*` stands for all capabilities reachable through `x`". - * We have `{x} <: {x*} <: dcs(x)}` where the deep capture set `dcs(x)` of `x` - * is the union of all capture sets that appear in covariant position in the - * type of `x`. If `x` and `y` are different variables then `{x*}` and `{y*}` - * are unrelated. - */ - def reach(using Context): CaptureRef = tp match - case tp: CaptureRef if tp.isTrackableRef => - if tp.isReach then tp else ReachCapability(tp) - /** If `x` is a capture ref, its maybe capability `x?`, represented internally * as `x @maybeCapability`. `x?` stands for a capability `x` that might or might * not be part of a capture set. We have `{} <: {x?} <: {x}`. Maybe capabilities @@ -436,62 +399,75 @@ extension (tp: Type) * but it has fewer issues with type inference. */ def maybe(using Context): CaptureRef = tp match - case tp: CaptureRef if tp.isTrackableRef => - if tp.isMaybe then tp else MaybeCapability(tp) + case tp @ AnnotatedType(_, annot) if annot.symbol == defn.MaybeCapabilityAnnot => tp + case _ => MaybeCapability(tp) - /** If `ref` is a trackable capture ref, and `tp` has only covariant occurrences of a - * universal capture set, replace all these occurrences by `{ref*}`. This implements - * the new aspect of the (Var) rule, which can now be stated as follows: - * - * x: T in E - * ----------- - * E |- x: T' - * - * where T' is T with (1) the toplevel capture set replaced by `{x}` and - * (2) all covariant occurrences of cap replaced by `x*`, provided there - * are no occurrences in `T` at other variances. (1) is standard, whereas - * (2) is new. - * - * For (2), multiple-flipped covariant occurrences of cap won't be replaced. - * In other words, - * - * - For xs: List[File^] ==> List[File^{xs*}], the cap is replaced; - * - while f: [R] -> (op: File^ => R) -> R remains unchanged. - * - * Without this restriction, the signature of functions like withFile: - * - * (path: String) -> [R] -> (op: File^ => R) -> R - * - * could be refined to - * - * (path: String) -> [R] -> (op: File^{withFile*} => R) -> R + /** If `x` is a capture ref, its reach capability `x*`, represented internally + * as `x @reachCapability`. `x*` stands for all capabilities reachable through `x`". + * We have `{x} <: {x*} <: dcs(x)}` where the deep capture set `dcs(x)` of `x` + * is the union of all capture sets that appear in covariant position in the + * type of `x`. If `x` and `y` are different variables then `{x*}` and `{y*}` + * are unrelated. * - * which is clearly unsound. + * Reach capabilities cannot wrap read-only capabilities or maybe capabilities. + * We have + * (x.rd).reach = x*.rd + * (x.rd)? = (x*)? + */ + def reach(using Context): CaptureRef = tp match + case tp @ AnnotatedType(tp1: CaptureRef, annot) + if annot.symbol == defn.MaybeCapabilityAnnot => + tp1.reach.maybe + case tp @ AnnotatedType(tp1: CaptureRef, annot) + if annot.symbol == defn.ReadOnlyCapabilityAnnot => + tp1.reach.readOnly + case tp @ AnnotatedType(tp1: CaptureRef, annot) + if annot.symbol == defn.ReachCapabilityAnnot => + tp + case _ => + ReachCapability(tp) + + /** If `x` is a capture ref, its read-only capability `x.rd`, represented internally + * as `x @readOnlyCapability`. We have {x.rd} <: {x}. If `x` is a reach capability `y*`, + * then its read-only version is `x.rd*`. * - * Why is this sound? Covariant occurrences of cap must represent capabilities - * that are reachable from `x`, so they are included in the meaning of `{x*}`. - * At the same time, encapsulation is still maintained since no covariant - * occurrences of cap are allowed in instance types of type variables. + * Read-only capabilities cannot wrap maybe capabilities + * but they can wrap reach capabilities. We have + * (x?).readOnly = (x.rd)? + */ + def readOnly(using Context): CaptureRef = tp match + case tp @ AnnotatedType(tp1: CaptureRef, annot) + if annot.symbol == defn.MaybeCapabilityAnnot => + tp1.readOnly.maybe + case tp @ AnnotatedType(tp1: CaptureRef, annot) + if annot.symbol == defn.ReadOnlyCapabilityAnnot => + tp + case _ => + ReadOnlyCapability(tp) + + /** If `x` is a capture ref, replace all no-flip covariant occurrences of `cap` + * in type `tp` with `x*`. */ def withReachCaptures(ref: Type)(using Context): Type = object narrowCaps extends TypeMap: var change = false def apply(t: Type) = if variance <= 0 then t - else t.dealiasKeepAnnots match - case t @ CapturingType(p, cs) if cs.isUniversal => + else t.dealias match + case t @ CapturingType(p, cs) if cs.containsRootCapability => change = true - t.derivedCapturingType(apply(p), ref.reach.singletonCaptureSet) + val reachRef = if cs.isReadOnly then ref.reach.readOnly else ref.reach + t.derivedCapturingType(apply(p), reachRef.singletonCaptureSet) case t @ AnnotatedType(parent, ann) => // Don't map annotations, which includes capture sets t.derivedAnnotatedType(this(parent), ann) - case t @ FunctionOrMethod(args, res @ Existential(_, _)) - if args.forall(_.isAlwaysPure) => - // Also map existentials in results to reach capabilities if all - // preceding arguments are known to be always pure - apply(t.derivedFunctionOrMethod(args, Existential.toCap(res))) - case Existential(_, _) => - t + case t @ FunctionOrMethod(args, res) => + if args.forall(_.isAlwaysPure) then + // Also map existentials in results to reach capabilities if all + // preceding arguments are known to be always pure + t.derivedFunctionOrMethod(args, apply(root.resultToFresh(res))) + else + t case _ => mapOver(t) end narrowCaps @@ -506,12 +482,33 @@ extension (tp: Type) tp case _ => tp + end withReachCaptures + + /** Does this type contain no-flip covariant occurrences of `cap`? */ + def containsCap(using Context): Boolean = + val acc = new TypeAccumulator[Boolean]: + def apply(x: Boolean, t: Type) = + x + || variance > 0 && t.dealiasKeepAnnots.match + case t @ CapturingType(p, cs) if cs.containsCap => + true + case t @ AnnotatedType(parent, ann) => + // Don't traverse annotations, which includes capture sets + this(x, parent) + case _ => + foldOver(x, t) + acc(false, tp) - def level(using Context): Level = + def level(using Context): CCState.Level = tp match - case tp: TermRef => tp.symbol.ccLevel - case tp: ThisType => tp.cls.ccLevel.nextInner - case _ => undefinedLevel + case tp: TermRef => ccState.symLevel(tp.symbol) + case tp: ThisType => ccState.symLevel(tp.cls).nextInner + case _ => CCState.undefinedLevel + +extension (tp: MethodType) + /** A method marks an existential scope unless it is the prefix of a curried method */ + def marksExistentialScope(using Context): Boolean = + !tp.resType.isInstanceOf[MethodOrPoly] extension (cls: ClassSymbol) @@ -615,12 +612,25 @@ extension (sym: Symbol) case c: TypeRef => c.symbol == sym case _ => false + def isUpdateMethod(using Context): Boolean = + sym.isAllOf(Mutable | Method, butNot = Accessor) + + def isReadOnlyMethod(using Context): Boolean = + sym.is(Method, butNot = Mutable | Accessor) && sym.owner.derivesFrom(defn.Caps_Mutable) + + def isInReadOnlyMethod(using Context): Boolean = + if sym.is(Method) && sym.owner.isClass then isReadOnlyMethod + else sym.owner.isInReadOnlyMethod + extension (tp: AnnotatedType) /** Is this a boxed capturing type? */ def isBoxed(using Context): Boolean = tp.annot match case ann: CaptureAnnotation => ann.boxed case _ => false + def rootAnnot: root.Annot = (tp.annot: @unchecked) match + case ann: root.Annot => ann + /** Drop retains annotations in the type. */ class CleanupRetains(using Context) extends TypeMap: def apply(tp: Type): Type = @@ -650,6 +660,14 @@ object ReachCapabilityApply: case Apply(reach, arg :: Nil) if reach.symbol == defn.Caps_reachCapability => Some(arg) case _ => None +/** An extractor for `caps.readOnlyCapability(ref)`, which is used to express a read-only + * capability as a tree in a @retains annotation. + */ +object ReadOnlyCapabilityApply: + def unapply(tree: Apply)(using Context): Option[Tree] = tree match + case Apply(ro, arg :: Nil) if ro.symbol == defn.Caps_readOnlyCapability => Some(arg) + case _ => None + /** An extractor for `caps.capsOf[X]`, which is used to express a generic capture set * as a tree in a @retains annotation. */ @@ -658,49 +676,41 @@ object CapsOfApply: case TypeApply(capsOf, arg :: Nil) if capsOf.symbol == defn.Caps_capsOf => Some(arg) case _ => None -class AnnotatedCapability(annot: Context ?=> ClassSymbol): - def apply(tp: Type)(using Context) = - AnnotatedType(tp, Annotation(annot, util.Spans.NoSpan)) +abstract class AnnotatedCapability(annotCls: Context ?=> ClassSymbol): + def apply(tp: Type)(using Context): AnnotatedType = + assert(tp.isTrackableRef, i"not a trackable ref: $tp") + tp match + case AnnotatedType(_, annot) => + assert(!unwrappable.contains(annot.symbol), i"illegal combination of derived capabilities: $annotCls over ${annot.symbol}") + case _ => + tp match + case tp: CaptureRef => tp.derivedRef(annotCls) + case _ => AnnotatedType(tp, Annotation(annotCls, util.Spans.NoSpan)) + def unapply(tree: AnnotatedType)(using Context): Option[CaptureRef] = tree match - case AnnotatedType(parent: CaptureRef, ann) if ann.symbol == annot => Some(parent) + case AnnotatedType(parent: CaptureRef, ann) if ann.hasSymbol(annotCls) => Some(parent) case _ => None -/** An extractor for `ref @annotation.internal.reachCapability`, which is used to express - * the reach capability `ref*` as a type. - */ -object ReachCapability extends AnnotatedCapability(defn.ReachCapabilityAnnot) + protected def unwrappable(using Context): Set[Symbol] +end AnnotatedCapability /** An extractor for `ref @maybeCapability`, which is used to express * the maybe capability `ref?` as a type. */ -object MaybeCapability extends AnnotatedCapability(defn.MaybeCapabilityAnnot) +object MaybeCapability extends AnnotatedCapability(defn.MaybeCapabilityAnnot): + protected def unwrappable(using Context) = Set() -/** Offers utility method to be used for type maps that follow aliases */ -trait ConservativeFollowAliasMap(using Context) extends TypeMap: +/** An extractor for `ref @readOnlyCapability`, which is used to express + * the read-only capability `ref.rd` as a type. + */ +object ReadOnlyCapability extends AnnotatedCapability(defn.ReadOnlyCapabilityAnnot): + protected def unwrappable(using Context) = Set(defn.MaybeCapabilityAnnot) - /** If `mapped` is a type alias, apply the map to the alias, while keeping - * annotations. If the result is different, return it, otherwise return `mapped`. - * Furthermore, if `original` is a LazyRef or TypeVar and the mapped result is - * the same as the underlying type, keep `original`. This avoids spurious differences - * which would lead to spurious dealiasing in the result - */ - protected def applyToAlias(original: Type, mapped: Type) = - val mapped1 = mapped match - case t: (TypeRef | AppliedType) => - val t1 = t.dealiasKeepAnnots - if t1 eq t then t - else - // If we see a type alias, map the alias type and keep it if it's different - val t2 = apply(t1) - if t2 ne t1 then t2 else t - case _ => - mapped - original match - case original: (LazyRef | TypeVar) if mapped1 eq original.underlying => - original - case _ => - mapped1 -end ConservativeFollowAliasMap +/** An extractor for `ref @annotation.internal.reachCapability`, which is used to express + * the reach capability `ref*` as a type. + */ +object ReachCapability extends AnnotatedCapability(defn.ReachCapabilityAnnot): + protected def unwrappable(using Context) = Set(defn.MaybeCapabilityAnnot, defn.ReadOnlyCapabilityAnnot) /** An extractor for all kinds of function types as well as method and poly types. * It includes aliases of function types such as `=>`. TODO: Can we do without? @@ -755,3 +765,36 @@ object ContainsParam: if tycon.typeSymbol == defn.Caps_ContainsTrait && cs.typeSymbol.isAbstractOrParamType => Some((cs, ref)) case _ => None + +/** A class encapsulating the assumulator logic needed for `CaptureSet.ofTypeDeeply` + * and `derivesFromCapTraitDeeply`. + * NOTE: The traversal logic needs to be in sync with narrowCaps in CaptureOps, which + * replaces caps with reach capabilties. There are two exceptions, however. + * - First, invariant arguments. These have to be included to be conservative + * in dcs but must be excluded in narrowCaps. + * - Second, unconstrained type variables are handled specially in `ofTypeDeeply`. + */ +abstract class DeepTypeAccumulator[T](using Context) extends TypeAccumulator[T]: + val seen = util.HashSet[Symbol]() + + protected def capturingCase(acc: T, parent: Type, refs: CaptureSet): T + + protected def abstractTypeCase(acc: T, t: TypeRef, upperBound: Type): T + + def apply(acc: T, t: Type) = + if variance < 0 then acc + else t.dealias match + case t @ CapturingType(p, cs1) => + capturingCase(acc, p, cs1) + case t: TypeRef if t.symbol.isAbstractOrParamType && !seen.contains(t.symbol) => + seen += t.symbol + abstractTypeCase(acc, t, t.info.bounds.hi) + case AnnotatedType(parent, _) => + this(acc, parent) + case t @ FunctionOrMethod(args, res) => + if args.forall(_.isAlwaysPure) then this(acc, root.resultToFresh(res)) + else acc + case _ => + foldOver(acc, t) +end DeepTypeAccumulator + diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 2caba4cf7d89..98f1502a0c1c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -6,62 +6,123 @@ import core.* import Types.*, Symbols.*, Contexts.*, Decorators.* import util.{SimpleIdentitySet, Property} import typer.ErrorReporting.Addenda -import TypeComparer.subsumesExistentially import util.common.alwaysTrue import scala.collection.mutable import CCState.* -import Periods.NoRunId +import Periods.{NoRunId, RunWidth} import compiletime.uninitialized import StdNames.nme +import CaptureSet.VarState +import Annotations.Annotation +import config.Printers.capt + +object CaptureRef: + opaque type Validity = Int + def validId(runId: Int, iterId: Int): Validity = + runId + (iterId << RunWidth) + def currentId(using Context): Validity = validId(ctx.runId, ccState.iterationId) + val invalid: Validity = validId(NoRunId, 0) /** A trait for references in CaptureSets. These can be NamedTypes, ThisTypes or ParamRefs, - * as well as two kinds of AnnotatedTypes representing reach and maybe capabilities. + * as well as three kinds of AnnotatedTypes representing readOnly, reach, and maybe capabilities. + * If there are several annotations they come with an order: + * `*` first, `.rd` next, `?` last. */ trait CaptureRef extends TypeProxy, ValueType: + import CaptureRef.* + private var myCaptureSet: CaptureSet | Null = uninitialized - private var myCaptureSetRunId: Int = NoRunId + private var myCaptureSetValid: Validity = invalid private var mySingletonCaptureSet: CaptureSet.Const | Null = null + private var myDerivedRefs: List[AnnotatedType] = Nil + + /** A derived reach, readOnly or maybe reference. Derived references are cached. */ + def derivedRef(annotCls: ClassSymbol)(using Context): AnnotatedType = + def recur(refs: List[AnnotatedType]): AnnotatedType = refs match + case ref :: refs1 => + if ref.annot.symbol == annotCls then ref else recur(refs1) + case Nil => + val derived = AnnotatedType(this, Annotation(annotCls, util.Spans.NoSpan)) + myDerivedRefs = derived :: myDerivedRefs + derived + recur(myDerivedRefs) /** Is the reference tracked? This is true if it can be tracked and the capture * set of the underlying type is not always empty. */ final def isTracked(using Context): Boolean = - this.isTrackableRef && (isMaxCapability || !captureSetOfInfo.isAlwaysEmpty) + this.isTrackableRef && (isRootCapability || !captureSetOfInfo.isAlwaysEmpty) + + /** Is this a maybe reference of the form `x?`? */ + final def isMaybe(using Context): Boolean = this ne stripMaybe + + /** Is this a read-only reference of the form `x.rd` or a capture set variable + * with only read-ony references in its upper bound? + */ + final def isReadOnly(using Context): Boolean = this match + case tp: TypeRef => tp.captureSetOfInfo.isReadOnly + case _ => this ne stripReadOnly /** Is this a reach reference of the form `x*`? */ - final def isReach(using Context): Boolean = this match - case AnnotatedType(_, annot) => annot.symbol == defn.ReachCapabilityAnnot - case _ => false + final def isReach(using Context): Boolean = this ne stripReach - /** Is this a maybe reference of the form `x?`? */ - final def isMaybe(using Context): Boolean = this match - case AnnotatedType(_, annot) => annot.symbol == defn.MaybeCapabilityAnnot - case _ => false + final def stripMaybe(using Context): CaptureRef = this match + case AnnotatedType(tp1: CaptureRef, annot) if annot.symbol == defn.MaybeCapabilityAnnot => + tp1 + case _ => + this - final def stripReach(using Context): CaptureRef = - if isReach then - val AnnotatedType(parent: CaptureRef, _) = this: @unchecked - parent - else this + final def stripReadOnly(using Context): CaptureRef = this match + case tp @ AnnotatedType(tp1: CaptureRef, annot) => + val sym = annot.symbol + if sym == defn.ReadOnlyCapabilityAnnot then + tp1 + else if sym == defn.MaybeCapabilityAnnot then + tp.derivedAnnotatedType(tp1.stripReadOnly, annot) + else + this + case _ => + this - final def stripMaybe(using Context): CaptureRef = - if isMaybe then - val AnnotatedType(parent: CaptureRef, _) = this: @unchecked - parent - else this + final def stripReach(using Context): CaptureRef = this match + case tp @ AnnotatedType(tp1: CaptureRef, annot) => + val sym = annot.symbol + if sym == defn.ReachCapabilityAnnot then + tp1 + else if sym == defn.ReadOnlyCapabilityAnnot || sym == defn.MaybeCapabilityAnnot then + tp.derivedAnnotatedType(tp1.stripReach, annot) + else + this + case _ => + this /** Is this reference the generic root capability `cap` ? */ - final def isRootCapability(using Context): Boolean = this match + final def isCap(using Context): Boolean = this match case tp: TermRef => tp.name == nme.CAPTURE_ROOT && tp.symbol == defn.captureRoot case _ => false - /** Is this reference capability that does not derive from another capability ? */ - final def isMaxCapability(using Context): Boolean = this match - case tp: TermRef => tp.isRootCapability || tp.info.derivesFrom(defn.Caps_Exists) - case tp: TermParamRef => tp.underlying.derivesFrom(defn.Caps_Exists) + /** Is this reference a Fresh instance? */ + final def isFresh(using Context): Boolean = this match + case root.Fresh(_) => true case _ => false - // With the support of pathes, we don't need to normalize the `TermRef`s anymore. + /** Is this reference the generic root capability `cap` or a Fresh instance? */ + final def isCapOrFresh(using Context): Boolean = isCap || isFresh + + /** Is this reference one of the generic root capabilities `cap` or `cap.rd` ? */ + final def isRootCapability(using Context): Boolean = this match + case ReadOnlyCapability(tp1) => tp1.isRootCapability + case root(_) => true + case _ => isCap + + /** An exclusive capability is a capability that derives + * indirectly from a maximal capability without going through + * a read-only capability first. + */ + final def isExclusive(using Context): Boolean = + !isReadOnly && (isRootCapability || captureSetOfInfo.isExclusive) + + // With the support of paths, we don't need to normalize the `TermRef`s anymore. // /** Normalize reference so that it can be compared with `eq` for equality */ // final def normalizedRef(using Context): CaptureRef = this match // case tp @ AnnotatedType(parent: CaptureRef, annot) if tp.isTrackableRef => @@ -78,20 +139,24 @@ trait CaptureRef extends TypeProxy, ValueType: /** The capture set of the type underlying this reference */ final def captureSetOfInfo(using Context): CaptureSet = - if ctx.runId == myCaptureSetRunId then myCaptureSet.nn + if myCaptureSetValid == currentId then myCaptureSet.nn else if myCaptureSet.asInstanceOf[AnyRef] eq CaptureSet.Pending then CaptureSet.empty else myCaptureSet = CaptureSet.Pending val computed = CaptureSet.ofInfo(this) - if !isCaptureChecking || ctx.mode.is(Mode.IgnoreCaptures) || underlying.isProvisional then + if !isCaptureChecking + || ctx.mode.is(Mode.IgnoreCaptures) + || !underlying.exists + || underlying.isProvisional + then myCaptureSet = null else myCaptureSet = computed - myCaptureSetRunId = ctx.runId + myCaptureSetValid = currentId computed final def invalidateCaches() = - myCaptureSetRunId = NoRunId + myCaptureSetValid = invalid /** x subsumes x * x =:= y ==> x subsumes y @@ -104,35 +169,35 @@ trait CaptureRef extends TypeProxy, ValueType: * X: CapSet^c1...CapSet^c2, (CapSet^c1) subsumes y ==> X subsumes y * Y: CapSet^c1...CapSet^c2, x subsumes (CapSet^c2) ==> x subsumes Y * Contains[X, y] ==> X subsumes y - * - * TODO: Document cases with more comments. */ - final def subsumes(y: CaptureRef)(using Context): Boolean = + final def subsumes(y: CaptureRef)(using ctx: Context)(using vs: VarState = VarState.Separate): Boolean = + def subsumingRefs(x: Type, y: Type): Boolean = x match case x: CaptureRef => y match case y: CaptureRef => x.subsumes(y) case _ => false case _ => false - def viaInfo(info: Type)(test: Type => Boolean): Boolean = info.match + def viaInfo(info: Type)(test: Type => Boolean): Boolean = info.dealias match case info: SingletonCaptureRef => test(info) + case CapturingType(parent, _) => + if this.derivesFrom(defn.Caps_CapSet) then test(info) + /* + If `this` is a capture set variable `C^`, then it is possible that it can be + reached from term variables in a reachability chain through the context. + For instance, in `def test[C^](src: Foo^{C^}) = { val x: Foo^{src} = src; val y: Foo^{x} = x; y }` + we expect that `C^` subsumes `x` and `y` in the body of the method + (cf. test case cc-poly-varargs.scala for a more involved example). + */ + else viaInfo(parent)(test) case info: AndType => viaInfo(info.tp1)(test) || viaInfo(info.tp2)(test) case info: OrType => viaInfo(info.tp1)(test) && viaInfo(info.tp2)(test) - case info @ CapturingType(_,_) if this.derivesFrom(defn.Caps_CapSet) => - /* - If `this` is a capture set variable `C^`, then it is possible that it can be - reached from term variables in a reachability chain through the context. - For instance, in `def test[C^](src: Foo^{C^}) = { val x: Foo^{src} = src; val y: Foo^{x} = x; y }` - we expect that `C^` subsumes `x` and `y` in the body of the method - (cf. test case cc-poly-varargs.scala for a more involved example). - */ - test(info) case _ => false (this eq y) - || this.isRootCapability + || maxSubsumes(y, canAddHidden = !vs.isOpen) || y.match - case y: TermRef if !y.isRootCapability => + case y: TermRef if !y.isCap => y.prefix.match case ypre: CaptureRef => this.subsumes(ypre) @@ -150,6 +215,7 @@ trait CaptureRef extends TypeProxy, ValueType: case _ => false || viaInfo(y.info)(subsumingRefs(this, _)) case MaybeCapability(y1) => this.stripMaybe.subsumes(y1) + case ReadOnlyCapability(y1) => this.stripReadOnly.subsumes(y1) case y: TypeRef if y.derivesFrom(defn.Caps_CapSet) => // The upper and lower bounds don't have to be in the form of `CapSet^{...}`. // They can be other capture set variables, which are bounded by `CapSet`, @@ -167,7 +233,6 @@ trait CaptureRef extends TypeProxy, ValueType: || this.match case ReachCapability(x1) => x1.subsumes(y.stripReach) case x: TermRef => viaInfo(x.info)(subsumingRefs(_, y)) - case x: TermParamRef => subsumesExistentially(x, y) case x: TypeRef if assumedContainsOf(x).contains(y) => true case x: TypeRef if x.derivesFrom(defn.Caps_CapSet) => x.info match @@ -180,6 +245,82 @@ trait CaptureRef extends TypeProxy, ValueType: case _ => false end subsumes + /** This is a maximal capability that subsumes `y` in given context and VarState. + * @param canAddHidden If true we allow maximal capabilities to subsume all other capabilities. + * We add those capabilities to the hidden set if this is a Fresh instance. + * If false we only accept `y` elements that are already in the + * hidden set of this Fresh instance. The idea is that in a VarState that + * accepts additions we first run `maxSubsumes` with `canAddHidden = false` + * so that new variables get added to the sets. If that fails, we run + * the test again with canAddHidden = true as a last effort before we + * fail a comparison. + */ + def maxSubsumes(y: CaptureRef, canAddHidden: Boolean)(using ctx: Context)(using vs: VarState = VarState.Separate): Boolean = + def yIsExistential = y.stripReadOnly match + case root.Result(_) => + capt.println(i"failed existential $this >: $y") + true + case _ => false + (this eq y) + || this.match + case root.Fresh(hidden) => + vs.ifNotSeen(this)(hidden.elems.exists(_.subsumes(y))) + || !y.stripReadOnly.isCap + && !yIsExistential + && !y.isInstanceOf[TermParamRef] + && canAddHidden + && vs.addHidden(hidden, y) + case x @ root.Result(binder) => + val result = y match + case y @ root.Result(_) => vs.unify(x, y) + case _ => y.derivesFromSharedCapability + if !result then + ccState.addNote(CaptureSet.ExistentialSubsumesFailure(x, y)) + result + case _ => + y match + case ReadOnlyCapability(y1) => this.stripReadOnly.maxSubsumes(y1, canAddHidden) + case _ if this.isCap => + y.isCap + || y.derivesFromSharedCapability + || !yIsExistential + && canAddHidden + && vs != VarState.HardSeparate + && (CCState.capIsRoot + // || { println(i"no longer $this maxSubsumes $y, ${y.isCap}"); false } // debug + ) + || false + case _ => + false + + /** `x covers y` if we should retain `y` when computing the overlap of + * two footprints which have `x` respectively `y` as elements. + * We assume that .rd have already been stripped on both sides. + * We have: + * + * x covers x + * x covers y ==> x covers y.f + * x covers y ==> x* covers y*, x? covers y? + * TODO what other clauses from subsumes do we need to port here? + */ + final def covers(y: CaptureRef)(using Context): Boolean = + (this eq y) + || y.match + case y @ TermRef(ypre: CaptureRef, _) if !y.isCap => + this.covers(ypre) + case ReachCapability(y1) => + this match + case ReachCapability(x1) => x1.covers(y1) + case _ => false + case MaybeCapability(y1) => + this match + case MaybeCapability(x1) => x1.covers(y1) + case _ => false + case root.Fresh(hidden) => + hidden.superCaps.exists(this covers _) + case _ => + false + def assumedContainsOf(x: TypeRef)(using Context): SimpleIdentitySet[CaptureRef] = CaptureSet.assumedContains.getOrElse(x, SimpleIdentitySet.empty) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRunInfo.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRunInfo.scala new file mode 100644 index 000000000000..06107992b592 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRunInfo.scala @@ -0,0 +1,25 @@ +package dotty.tools.dotc +package cc + +import core.Contexts.{Context, ctx} +import config.Printers.capt + +trait CaptureRunInfo: + self: Run => + private var maxSize = 0 + private var maxPath: List[CaptureSet.DerivedVar] = Nil + + def recordPath(size: Int, path: => List[CaptureSet.DerivedVar]): Unit = + if size > maxSize then + maxSize = size + maxPath = path + + def printMaxPath()(using Context): Unit = + if maxSize > 0 then + println(s"max derived capture set path length: $maxSize") + println(s"max derived capture set path: ${maxPath.map(_.summarize).reverse}") + + protected def reset(): Unit = + maxSize = 0 + maxPath = Nil +end CaptureRunInfo diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 39c41c369864..93ca0956baed 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -14,10 +14,11 @@ import printing.{Showable, Printer} import printing.Texts.* import util.{SimpleIdentitySet, Property} import typer.ErrorReporting.Addenda -import TypeComparer.subsumesExistentially import util.common.alwaysTrue import scala.collection.{mutable, immutable} import CCState.* +import TypeOps.AvoidMap +import compiletime.uninitialized /** A class for capture sets. Capture sets can be constants or variables. * Capture sets support inclusion constraints <:< where <:< is subcapturing. @@ -50,12 +51,15 @@ sealed abstract class CaptureSet extends Showable: /** Is this capture set constant (i.e. not an unsolved capture variable)? * Solved capture variables count as constant. */ - def isConst: Boolean + def isConst(using Context): Boolean /** Is this capture set always empty? For unsolved capture veriables, returns * always false. */ - def isAlwaysEmpty: Boolean + def isAlwaysEmpty(using Context): Boolean + + /** Is this set provisionally solved, so that another cc run might unfreeze it? */ + def isProvisionallySolved(using Context): Boolean /** An optional level limit, or undefinedLevel if none exists. All elements of the set * must be at levels equal or smaller than the level of the set, if it is defined. @@ -70,23 +74,38 @@ sealed abstract class CaptureSet extends Showable: final def isNotEmpty: Boolean = !elems.isEmpty /** Convert to Const. @pre: isConst */ - def asConst: Const = this match + def asConst(using Context): Const = this match case c: Const => c case v: Var => assert(v.isConst) Const(v.elems) /** Cast to variable. @pre: !isConst */ - def asVar: Var = + def asVar(using Context): Var = assert(!isConst) asInstanceOf[Var] + /** Convert to Const with current elements unconditionally */ + def toConst: Const = this match + case c: Const => c + case v: Var => Const(v.elems) + /** Does this capture set contain the root reference `cap` as element? */ final def isUniversal(using Context) = + elems.exists(_.isCap) + + /** Does this capture set contain a root reference `cap` or `cap.rd` as element? */ + final def containsRootCapability(using Context) = elems.exists(_.isRootCapability) - final def isUnboxable(using Context) = - elems.exists(elem => elem.isRootCapability || Existential.isExistentialVar(elem)) + final def containsCap(using Context) = + elems.exists(_.stripReadOnly.isCap) + + final def isReadOnly(using Context): Boolean = + elems.forall(_.isReadOnly) + + final def isExclusive(using Context): Boolean = + elems.exists(_.isExclusive) final def keepAlways: Boolean = this.isInstanceOf[EmptyWithProvenance] @@ -125,8 +144,8 @@ sealed abstract class CaptureSet extends Showable: * element is not the root capability, try instead to include its underlying * capture set. */ - protected final def addNewElem(elem: CaptureRef)(using Context, VarState): CompareResult = - if elem.isMaxCapability || summon[VarState] == FrozenState then + protected final def addNewElem(elem: CaptureRef)(using ctx: Context, vs: VarState): CompareResult = + if elem.isRootCapability || !vs.isOpen then addThisElem(elem) else addThisElem(elem).orElse: @@ -146,27 +165,40 @@ sealed abstract class CaptureSet extends Showable: */ protected def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult + protected def addIfHiddenOrFail(elem: CaptureRef)(using ctx: Context, vs: VarState): CompareResult = + if elems.exists(_.maxSubsumes(elem, canAddHidden = true)) + then CompareResult.OK + else CompareResult.Fail(this :: Nil) + /** If this is a variable, add `cs` as a dependent set */ protected def addDependent(cs: CaptureSet)(using Context, VarState): CompareResult /** If `cs` is a variable, add this capture set as one of its dependent sets */ protected def addAsDependentTo(cs: CaptureSet)(using Context): this.type = - cs.addDependent(this)(using ctx, UnrecordedState) + cs.addDependent(this)(using ctx, VarState.Unrecorded) this /** {x} <:< this where <:< is subcapturing, but treating all variables * as frozen. */ - def accountsFor(x: CaptureRef)(using Context): Boolean = + def accountsFor(x: CaptureRef)(using ctx: Context)(using vs: VarState = VarState.Separate): Boolean = + def debugInfo(using Context) = i"$this accountsFor $x, which has capture set ${x.captureSetOfInfo}" + def test(using Context) = reporting.trace(debugInfo): elems.exists(_.subsumes(x)) - || !x.isMaxCapability + || // Even though subsumes already follows captureSetOfInfo, this is not enough. + // For instance x: C^{y, z}. Then neither y nor z subsumes x but {y, z} accounts for x. + !x.isRootCapability && !x.derivesFrom(defn.Caps_CapSet) - && x.captureSetOfInfo.subCaptures(this, frozen = true).isOK + && !(vs.isSeparating && x.captureSetOfInfo.containsRootCapability) + // in VarState.Separate, don't try to widen to cap since that might succeed with {cap} <: {cap} + && x.captureSetOfInfo.subCaptures(this, VarState.Separate).isOK + comparer match case comparer: ExplainingTypeComparer => comparer.traceIndented(debugInfo)(test) case _ => test + end accountsFor /** A more optimistic version of accountsFor, which does not take variable supersets * of the `x` reference into account. A set might account for `x` if it accounts @@ -176,14 +208,14 @@ sealed abstract class CaptureSet extends Showable: * root capability `cap`. */ def mightAccountFor(x: CaptureRef)(using Context): Boolean = - reporting.trace(i"$this mightAccountFor $x, ${x.captureSetOfInfo}?", show = true) { - elems.exists(_.subsumes(x)) - || !x.isMaxCapability + reporting.trace(i"$this mightAccountFor $x, ${x.captureSetOfInfo}?", show = true): + CCState.withCapAsRoot: // OK here since we opportunistically choose an alternative which gets checked later + elems.exists(_.subsumes(x)(using ctx)(using VarState.ClosedUnrecorded)) + || !x.isRootCapability && { val elems = x.captureSetOfInfo.elems !elems.isEmpty && elems.forall(mightAccountFor) } - } /** A more optimistic version of subCaptures used to choose one of two typing rules * for selections and applications. `cs1 mightSubcapture cs2` if `cs2` might account for @@ -194,21 +226,17 @@ sealed abstract class CaptureSet extends Showable: elems.forall(that.mightAccountFor) && !that.elems.forall(this.mightAccountFor) - /** The subcapturing test. - * @param frozen if true, no new variables or dependent sets are allowed to - * be added when making this test. An attempt to add either - * will result in failure. - */ - final def subCaptures(that: CaptureSet, frozen: Boolean)(using Context): CompareResult = - subCaptures(that)(using ctx, if frozen then FrozenState else VarState()) + /** The subcapturing test, taking an explicit VarState. */ + final def subCaptures(that: CaptureSet, vs: VarState)(using Context): CompareResult = + subCaptures(that)(using ctx, vs) /** The subcapturing test, using a given VarState */ - private def subCaptures(that: CaptureSet)(using Context, VarState): CompareResult = + final def subCaptures(that: CaptureSet)(using ctx: Context, vs: VarState = VarState()): CompareResult = val result = that.tryInclude(elems, this) if result.isOK then addDependent(that) else - ccState.levelError = ccState.levelError.orElse(result.levelError) + result.levelError.foreach(ccState.addNote) varState.rollBack() result //.showing(i"subcaptures $this <:< $that = ${result.show}", capt) @@ -217,19 +245,22 @@ sealed abstract class CaptureSet extends Showable: * in a frozen state. */ def =:= (that: CaptureSet)(using Context): Boolean = - this.subCaptures(that, frozen = true).isOK - && that.subCaptures(this, frozen = true).isOK + this.subCaptures(that, VarState.Separate).isOK + && that.subCaptures(this, VarState.Separate).isOK /** The smallest capture set (via <:<) that is a superset of both * `this` and `that` */ def ++ (that: CaptureSet)(using Context): CaptureSet = - if this.subCaptures(that, frozen = true).isOK then + if this.subCaptures(that, VarState.HardSeparate).isOK then if that.isAlwaysEmpty && this.keepAlways then this else that - else if that.subCaptures(this, frozen = true).isOK then this + else if that.subCaptures(this, VarState.HardSeparate).isOK then this else if this.isConst && that.isConst then Const(this.elems ++ that.elems) else Union(this, that) + def ++ (that: CaptureSet.Const)(using Context): CaptureSet.Const = + Const(this.elems ++ that.elems) + /** The smallest superset (via <:<) of this capture set that also contains `ref`. */ def + (ref: CaptureRef)(using Context): CaptureSet = @@ -238,8 +269,8 @@ sealed abstract class CaptureSet extends Showable: /** The largest capture set (via <:<) that is a subset of both `this` and `that` */ def **(that: CaptureSet)(using Context): CaptureSet = - if this.subCaptures(that, frozen = true).isOK then this - else if that.subCaptures(this, frozen = true).isOK then that + if this.subCaptures(that, VarState.Closed()).isOK then this + else if that.subCaptures(this, VarState.Closed()).isOK then that else if this.isConst && that.isConst then Const(elemIntersection(this, that)) else Intersection(this, that) @@ -265,44 +296,52 @@ sealed abstract class CaptureSet extends Showable: val elems1 = elems.filter(p) if elems1 == elems then this else Const(elems.filter(p)) - else Filtered(asVar, p) + else + this match + case self: Filtered => Filtered(self.source, ref => self.p(ref) && p(ref)) + case _ => Filtered(asVar, p) /** Capture set obtained by applying `tm` to all elements of the current capture set - * and joining the results. If the current capture set is a variable, the same - * transformation is applied to all future additions of new elements. - * - * Note: We have a problem how we handle the situation where we have a mapped set - * - * cs2 = tm(cs1) - * - * and then the propagation solver adds a new element `x` to `cs2`. What do we - * know in this case about `cs1`? We can answer this question in a sound way only - * if `tm` is a bijection on capture references or it is idempotent on capture references. - * (see definition in IdempotentCapRefMap). - * If `tm` is a bijection we know that `tm^-1(x)` must be in `cs1`. If `tm` is idempotent - * one possible solution is that `x` is in `cs1`, which is what we assume in this case. - * That strategy is sound but not complete. - * - * If `tm` is some other map, we don't know how to handle this case. For now, - * we simply refuse to handle other maps. If they do need to be handled, - * `OtherMapped` provides some approximation to a solution, but it is neither - * sound nor complete. + * and joining the results. If the current capture set is a variable we handle this as + * follows: + * - If the map is a BiTypeMap, the same transformation is applied to all + * future additions of new elements. We try to fuse with previous maps to + * avoid long paths of BiTypeMapped sets. + * - If the map is some other map that maps the current set of elements + * to itself, return the current var. We implicitly assume that the map + * will also map any elements added in the future to themselves. This assumption + * can be tested to hold by setting the ccConfig.checkSkippedMaps setting to true. + * - If the map is some other map that does not map all elements to themselves, + * freeze the current set (i.e. make it porvisionally solved) and return + * the mapped elements as a constant set. */ - def map(tm: TypeMap)(using Context): CaptureSet = tm match - case tm: BiTypeMap => - val mappedElems = elems.map(tm.forward) - if isConst then - if mappedElems == elems then this - else Const(mappedElems) - else BiMapped(asVar, tm, mappedElems) - case tm: IdentityCaptRefMap => - this - case _ => - val mapped = mapRefs(elems, tm, tm.variance) - if isConst then - if mapped.isConst && mapped.elems == elems && !mapped.keepAlways then this - else mapped - else Mapped(asVar, tm, tm.variance, mapped) + def map(tm: TypeMap)(using Context): CaptureSet = + tm match + case tm: BiTypeMap => + val mappedElems = elems.map(tm.forward) + if isConst then + if mappedElems == elems then this + else Const(mappedElems) + else if ccState.mapFutureElems then + def unfused = BiMapped(asVar, tm, mappedElems) + this match + case self: BiMapped => self.bimap.fuse(tm) match + case Some(fused: BiTypeMap) => BiMapped(self.source, fused, mappedElems) + case _ => unfused + case _ => unfused + else this + case tm: IdentityCaptRefMap => + this + case tm: AvoidMap if this.isInstanceOf[HiddenSet] => + this + case _ => + val mapped = mapRefs(elems, tm, tm.variance) + if mapped.elems == elems then + if ccConfig.checkSkippedMaps && !isConst then asVar.skippedMaps += tm + this + else + if !isConst then asVar.markSolved(provisional = true) + mapped /** A mapping resulting from substituting parameters of a BindingType to a list of types */ def substParams(tl: BindingType, to: List[Type])(using Context) = @@ -310,9 +349,11 @@ sealed abstract class CaptureSet extends Showable: def maybe(using Context): CaptureSet = map(MaybeMap()) + def readOnly(using Context): CaptureSet = map(ReadOnlyMap()) + /** Invoke handler if this set has (or later aquires) the root capability `cap` */ def disallowRootCapability(handler: () => Context ?=> Unit)(using Context): this.type = - if isUnboxable then handler() + if containsRootCapability then handler() this /** Invoke handler on the elements to ensure wellformedness of the capture set. @@ -336,7 +377,7 @@ sealed abstract class CaptureSet extends Showable: * to this set. This might result in the set being solved to be constant * itself. */ - protected def propagateSolved()(using Context): Unit = () + protected def propagateSolved(provisional: Boolean)(using Context): Unit = () /** This capture set with a description that tells where it comes from */ def withDescription(description: String): CaptureSet @@ -354,42 +395,65 @@ sealed abstract class CaptureSet extends Showable: override def toText(printer: Printer): Text = printer.toTextCaptureSet(this) ~~ description + /** Apply function `f` to the elements. Typically used for printing. + * Overridden in HiddenSet so that we don't run into infinite recursions + */ + def processElems[T](f: Refs => T): T = f(elems) + object CaptureSet: type Refs = SimpleIdentitySet[CaptureRef] type Vars = SimpleIdentitySet[Var] type Deps = SimpleIdentitySet[CaptureSet] - @sharable private var varId = 0 - /** If set to `true`, capture stack traces that tell us where sets are created */ private final val debugSets = false - private val emptySet = SimpleIdentitySet.empty + val emptyRefs: Refs = SimpleIdentitySet.empty /** The empty capture set `{}` */ - val empty: CaptureSet.Const = Const(emptySet) + val empty: CaptureSet.Const = Const(emptyRefs) /** The universal capture set `{cap}` */ def universal(using Context): CaptureSet = - defn.captureRoot.termRef.singletonCaptureSet + root.cap.singletonCaptureSet + + /** The same as CaptureSet.universal but generated implicitly for + * references of Capability subtypes + */ + def universalImpliedByCapability(using Context) = + defn.universalCSImpliedByCapability + + def fresh(owner: Symbol = NoSymbol)(using Context): CaptureSet = + root.Fresh.withOwner(owner).singletonCaptureSet + + /** The shared capture set `{cap.rd}` */ + def shared(using Context): CaptureSet = + root.cap.readOnly.singletonCaptureSet /** Used as a recursion brake */ @sharable private[dotc] val Pending = Const(SimpleIdentitySet.empty) def apply(elems: CaptureRef*)(using Context): CaptureSet.Const = if elems.isEmpty then empty - else Const(SimpleIdentitySet(elems.map(_.ensuring(_.isTrackableRef))*)) + else + for elem <- elems do + assert(elem.isTrackableRef, i"not a trackable ref: $elem") + Const(SimpleIdentitySet(elems*)) def apply(elems: Refs)(using Context): CaptureSet.Const = if elems.isEmpty then empty else Const(elems) /** The subclass of constant capture sets with given elements `elems` */ class Const private[CaptureSet] (val elems: Refs, val description: String = "") extends CaptureSet: - def isConst = true - def isAlwaysEmpty = elems.isEmpty + def isConst(using Context) = true + def isAlwaysEmpty(using Context) = elems.isEmpty + def isProvisionallySolved(using Context) = false def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult = - CompareResult.Fail(this :: Nil) + val res = addIfHiddenOrFail(elem) + if !res.isOK && this.isProvisionallySolved then + println(i"Cannot add $elem to provisionally solved $this") + res def addDependent(cs: CaptureSet)(using Context, VarState) = CompareResult.OK @@ -416,37 +480,49 @@ object CaptureSet: * nulls, this provides more lenient checking against compilation units that * were not yet compiled with capture checking on. */ - object Fluid extends Const(emptySet): - override def isAlwaysEmpty = false + object Fluid extends Const(emptyRefs): + override def isAlwaysEmpty(using Context) = false override def addThisElem(elem: CaptureRef)(using Context, VarState) = CompareResult.OK - override def accountsFor(x: CaptureRef)(using Context): Boolean = true + override def accountsFor(x: CaptureRef)(using Context)(using VarState): Boolean = true override def mightAccountFor(x: CaptureRef)(using Context): Boolean = true override def toString = "" end Fluid /** The subclass of captureset variables with given initial elements */ - class Var(override val owner: Symbol = NoSymbol, initialElems: Refs = emptySet, val level: Level = undefinedLevel, underBox: Boolean = false)(using @constructorOnly ictx: Context) extends CaptureSet: + class Var(initialOwner: Symbol = NoSymbol, initialElems: Refs = emptyRefs, val level: Level = undefinedLevel, underBox: Boolean = false)(using @constructorOnly ictx: Context) extends CaptureSet: + + override def owner = initialOwner /** A unique identification number for diagnostics */ val id = - varId += 1 - varId + val ccs = ccState + ccs.varId += 1 + ccs.varId //assert(id != 40) - /** A variable is solved if it is aproximated to a from-then-on constant set. */ - private var isSolved: Boolean = false + /** A variable is solved if it is aproximated to a from-then-on constant set. + * Interpretation: + * 0 not solved + * Int.MaxValue definitively solved + * n > 0 provisionally solved in iteration n + */ + private var solved: Int = 0 /** The elements currently known to be in the set */ - var elems: Refs = initialElems + protected var myElems: Refs = initialElems + + def elems: Refs = myElems + def elems_=(refs: Refs): Unit = myElems = refs /** The sets currently known to be dependent sets (i.e. new additions to this set * are propagated to these dependent sets.) */ - var deps: Deps = emptySet + var deps: Deps = SimpleIdentitySet.empty - def isConst = isSolved - def isAlwaysEmpty = isSolved && elems.isEmpty + def isConst(using Context) = solved >= ccState.iterationId + def isAlwaysEmpty(using Context) = isConst && elems.isEmpty + def isProvisionallySolved(using Context): Boolean = solved > 0 && solved != Int.MaxValue def isMaybeSet = false // overridden in BiMapped @@ -484,17 +560,25 @@ object CaptureSet: def resetDeps()(using state: VarState): Unit = deps = state.deps(this) + /** Check that all maps recorded in skippedMaps map `elem` to itself + * or something subsumed by it. + */ + private def checkSkippedMaps(elem: CaptureRef)(using Context): Unit = + for tm <- skippedMaps do + val elem1 = tm(elem) + for elem1 <- tm(elem).captureSet.elems do + assert(elem.subsumes(elem1), + i"Skipped map ${tm.getClass} maps newly added $elem to $elem1 in $this") + final def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult = - if isConst // Fail if variable is solved, - || !recordElemsState() // or given VarState is frozen, - || Existential.isBadExistential(elem) // or `elem` is an out-of-scope existential, - then - CompareResult.Fail(this :: Nil) + if isConst || !recordElemsState() then // Fail if variable is solved or given VarState is frozen + addIfHiddenOrFail(elem) else if !levelOK(elem) then CompareResult.LevelError(this, elem) // or `elem` is not visible at the level of the set. else - //if id == 34 then assert(!elem.isUniversalRootCapability) + // id == 108 then assert(false, i"trying to add $elem to $this") assert(elem.isTrackableRef, elem) + assert(!this.isInstanceOf[HiddenSet] || summon[VarState].isSeparating, summon[VarState]) elems += elem if elem.isRootCapability then rootAddedHandler() @@ -503,29 +587,46 @@ object CaptureSet: // assert(id != 5 || elems.size != 3, this) val res = (CompareResult.OK /: deps): (r, dep) => r.andAlso(dep.tryInclude(normElem, this)) + if ccConfig.checkSkippedMaps && res.isOK then checkSkippedMaps(elem) res.orElse: elems -= elem res.addToTrace(this) + private def isPartOf(binder: Type)(using Context): Boolean = + val find = new TypeAccumulator[Boolean]: + def apply(b: Boolean, t: Type) = + b || t.match + case CapturingType(p, refs) => (refs eq Var.this) || this(b, p) + case _ => foldOver(b, t) + find(false, binder) + + // TODO: Also track allowable TermParamRefs and root.Results in capture sets private def levelOK(elem: CaptureRef)(using Context): Boolean = if elem.isRootCapability then !noUniversal - else if Existential.isExistentialVar(elem) then - !noUniversal - && !TypeComparer.isOpenedExistential(elem) - // Opened existentials on the left cannot be added to nested capture sets on the right - // of a comparison. Test case is open-existential.scala. else elem match + case elem @ root.Result(mt) => + !noUniversal && isPartOf(mt.resType) case elem: TermRef if level.isDefined => elem.prefix match case prefix: CaptureRef => levelOK(prefix) case _ => - elem.symbol.ccLevel <= level + ccState.symLevel(elem.symbol) <= level case elem: ThisType if level.isDefined => - elem.cls.ccLevel.nextInner <= level + ccState.symLevel(elem.cls).nextInner <= level + case elem: ParamRef if !this.isInstanceOf[BiMapped] => + isPartOf(elem.binder.resType) + || { + capt.println( + i"""LEVEL ERROR $elem for $this + |elem binder = ${elem.binder}""") + false + } case ReachCapability(elem1) => levelOK(elem1) + case ReadOnlyCapability(elem1) => + levelOK(elem1) case MaybeCapability(elem1) => levelOK(elem1) case _ => @@ -558,16 +659,21 @@ object CaptureSet: final def upperApprox(origin: CaptureSet)(using Context): CaptureSet = if isConst then this - else if elems.exists(_.isRootCapability) || computingApprox then + else if isUniversal || computingApprox then universal + else if containsCap && isReadOnly then + shared else computingApprox = true try val approx = computeApprox(origin).ensuring(_.isConst) - if approx.elems.exists(Existential.isExistentialVar(_)) then + if approx.elems.exists: + case root.Result(_) => true + case _ => false + then ccState.approxWarnings += em"""Capture set variable $this gets upper-approximated - |to existential variable from $approx, using {cap} instead.""" + |to existential variable from $approx, using {cap} instead.""" universal else approx finally computingApprox = false @@ -581,18 +687,22 @@ object CaptureSet: * in the results of defs and vals. */ def solve()(using Context): Unit = - if !isConst then + CCState.withCapAsRoot: // // OK here since we infer parameter types that get checked later val approx = upperApprox(empty) + .map(root.CapToFresh(NoSymbol).inverse) // Fresh --> cap .showing(i"solve $this = $result", capt) //println(i"solving var $this $approx ${approx.isConst} deps = ${deps.toList}") val newElems = approx.elems -- elems - if tryInclude(newElems, empty)(using ctx, VarState()).isOK then - markSolved() + given VarState() + if tryInclude(newElems, empty).isOK then + markSolved(provisional = false) /** Mark set as solved and propagate this info to all dependent sets */ - def markSolved()(using Context): Unit = - isSolved = true - deps.foreach(_.propagateSolved()) + def markSolved(provisional: Boolean)(using Context): Unit = + solved = if provisional then ccState.iterationId else Int.MaxValue + deps.foreach(_.propagateSolved(provisional)) + + var skippedMaps: Set[TypeMap] = Set.empty def withDescription(description: String): this.type = this.description = this.description.join(" and ", description) @@ -622,11 +732,13 @@ object CaptureSet: * is not derived from some other variable. */ protected def ids(using Context): String = + def descr = getClass.getSimpleName.nn.take(1) val trail = this.match - case dv: DerivedVar => dv.source.ids - case _ => "" - val descr = getClass.getSimpleName.nn.take(1) - s"$id$descr$trail" + case dv: DerivedVar => + def summary = if ctx.settings.YccVerbose.value then dv.summarize else descr + s"$summary${dv.source.ids}" + case _ => descr + s"$id$trail" override def toString = s"Var$id$elems" end Var @@ -642,120 +754,42 @@ object CaptureSet: extends Var(owner, initialElems): // For debugging: A trace where a set was created. Note that logically it would make more - // sense to place this variable in Mapped, but that runs afoul of the initializatuon checker. - val stack = if debugSets && this.isInstanceOf[Mapped] then (new Throwable).getStackTrace().nn.take(20) else null + // sense to place this variable in BiMapped, but that runs afoul of the initializatuon checker. + // val stack = if debugSets && this.isInstanceOf[BiMapped] then (new Throwable).getStackTrace().nn.take(20) else null /** The variable from which this variable is derived */ def source: Var addAsDependentTo(source) - override def propagateSolved()(using Context) = - if source.isConst && !isConst then markSolved() - end DerivedVar - - /** A variable that changes when `source` changes, where all additional new elements are mapped - * using ∪ { tm(x) | x <- source.elems }. - * @param source the original set that is mapped - * @param tm the type map, which is assumed to be idempotent on capture refs - * (except if ccUnsoundMaps is enabled) - * @param variance the assumed variance with which types with capturesets of size >= 2 are approximated - * (i.e. co: full capture set, contra: empty set, nonvariant is not allowed.) - * @param initial The initial mappings of source's elements at the point the Mapped set is created. - */ - class Mapped private[CaptureSet] - (val source: Var, tm: TypeMap, variance: Int, initial: CaptureSet)(using @constructorOnly ctx: Context) - extends DerivedVar(source.owner, initial.elems): - addAsDependentTo(initial) // initial mappings could change by propagation - - private def mapIsIdempotent = tm.isInstanceOf[IdempotentCaptRefMap] - - assert(ccConfig.allowUnsoundMaps || mapIsIdempotent, tm.getClass) + override def propagateSolved(provisional: Boolean)(using Context) = + if source.isConst && !isConst then markSolved(provisional) - private def whereCreated(using Context): String = - if stack == null then "" - else i""" - |Stack trace of variable creation:" - |${stack.mkString("\n")}""" - - override def tryInclude(elem: CaptureRef, origin: CaptureSet)(using Context, VarState): CompareResult = - def propagate: CompareResult = - if (origin ne source) && (origin ne initial) && mapIsIdempotent then - // `tm` is idempotent, propagate back elems from image set. - // This is sound, since we know that for `r in newElems: tm(r) = r`, hence - // `r` is _one_ possible solution in `source` that would make an `r` appear in this set. - // It's not necessarily the only possible solution, so the scheme is incomplete. - source.tryInclude(elem, this) - else if ccConfig.allowUnsoundMaps && !mapIsIdempotent - && variance <= 0 && !origin.isConst && (origin ne initial) && (origin ne source) - then - // The map is neither a BiTypeMap nor an idempotent type map. - // In that case there's no much we can do. - // The scheme then does not propagate added elements back to source and rejects adding - // elements from variable sources in contra- and non-variant positions. In essence, - // we approximate types resulting from such maps by returning a possible super type - // from the actual type. But this is neither sound nor complete. - report.warning(em"trying to add $elem from unrecognized source $origin of mapped set $this$whereCreated") - CompareResult.Fail(this :: Nil) - else - CompareResult.OK - def propagateIf(cond: Boolean): CompareResult = - if cond then propagate else CompareResult.OK + // ----------- Longest path recording ------------------------- - val mapped = extrapolateCaptureRef(elem, tm, variance) + /** Summarize for set displaying in a path */ + def summarize: String = getClass.toString - def isFixpoint = - mapped.isConst && mapped.elems.size == 1 && mapped.elems.contains(elem) + /** The length of the path of DerivedVars ending in this set */ + def pathLength: Int = source match + case source: DerivedVar => source.pathLength + 1 + case _ => 1 - def failNoFixpoint = - val reason = - if variance <= 0 then i"the set's variance is $variance" - else i"$elem gets mapped to $mapped, which is not a supercapture." - report.warning(em"""trying to add $elem from unrecognized source $origin of mapped set $this$whereCreated - |The reference cannot be added since $reason""") - CompareResult.Fail(this :: Nil) - - if origin eq source then // elements have to be mapped - val added = mapped.elems.filter(!accountsFor(_)) - addNewElems(added) - .andAlso: - if mapped.isConst then CompareResult.OK - else if mapped.asVar.recordDepsState() then { addAsDependentTo(mapped); CompareResult.OK } - else CompareResult.Fail(this :: Nil) - .andAlso: - propagateIf(!added.isEmpty) - else if accountsFor(elem) then - CompareResult.OK - else if variance > 0 then - // we can soundly add nothing to source and `x` to this set - addNewElem(elem) - else if isFixpoint then - // We can soundly add `x` to both this set and source since `f(x) = x` - addNewElem(elem).andAlso(propagate) - else - // we are out of options; fail (which is always sound). - failNoFixpoint - end tryInclude - - override def computeApprox(origin: CaptureSet)(using Context): CaptureSet = - if source eq origin then - // it's a mapping of origin, so not a superset of `origin`, - // therefore don't contribute to the intersection. - universal - else - source.upperApprox(this).map(tm) + /** The path of DerivedVars ending in this set */ + def path: List[DerivedVar] = source match + case source: DerivedVar => this :: source.path + case _ => this :: Nil - override def propagateSolved()(using Context) = - if initial.isConst then super.propagateSolved() + if ctx.settings.YccLog.value || util.Stats.enabled then + ctx.run.nn.recordPath(pathLength, path) - override def toString = s"Mapped$id($source, elems = $elems)" - end Mapped + end DerivedVar /** A mapping where the type map is required to be a bijection. * Parameters as in Mapped. */ final class BiMapped private[CaptureSet] - (val source: Var, bimap: BiTypeMap, initialElems: Refs)(using @constructorOnly ctx: Context) + (val source: Var, val bimap: BiTypeMap, initialElems: Refs)(using @constructorOnly ctx: Context) extends DerivedVar(source.owner, initialElems): override def tryInclude(elem: CaptureRef, origin: CaptureSet)(using Context, VarState): CompareResult = @@ -766,9 +800,13 @@ object CaptureSet: else if accountsFor(elem) then CompareResult.OK else - source.tryInclude(bimap.backward(elem), this) - .showing(i"propagating new elem $elem backward from $this to $source = $result", captDebug) - .andAlso(addNewElem(elem)) + try + source.tryInclude(bimap.backward(elem), this) + .showing(i"propagating new elem $elem backward from $this to $source = $result", captDebug) + .andAlso(addNewElem(elem)) + catch case ex: AssertionError => + println(i"fail while tryInclude $elem of ${elem.getClass} in $this / ${this.summarize}") + throw ex /** For a BiTypeMap, supertypes of the mapped type also constrain * the source via the inverse type mapping and vice versa. That is, if @@ -784,11 +822,12 @@ object CaptureSet: override def isMaybeSet: Boolean = bimap.isInstanceOf[MaybeMap] override def toString = s"BiMapped$id($source, elems = $elems)" + override def summarize = bimap.getClass.toString end BiMapped /** A variable with elements given at any time as { x <- source.elems | p(x) } */ class Filtered private[CaptureSet] - (val source: Var, p: Context ?=> CaptureRef => Boolean)(using @constructorOnly ctx: Context) + (val source: Var, val p: Context ?=> CaptureRef => Boolean)(using @constructorOnly ctx: Context) extends DerivedVar(source.owner, source.elems.filter(p)): override def tryInclude(elem: CaptureRef, origin: CaptureSet)(using Context, VarState): CompareResult = @@ -836,8 +875,8 @@ object CaptureSet: else res else res - override def propagateSolved()(using Context) = - if cs1.isConst && cs2.isConst && !isConst then markSolved() + override def propagateSolved(provisional: Boolean)(using Context) = + if cs1.isConst && cs2.isConst && !isConst then markSolved(provisional) end Union class Intersection(cs1: CaptureSet, cs2: CaptureSet)(using Context) @@ -863,12 +902,102 @@ object CaptureSet: else CaptureSet(elemIntersection(cs1.upperApprox(this), cs2.upperApprox(this))) - override def propagateSolved()(using Context) = - if cs1.isConst && cs2.isConst && !isConst then markSolved() + override def propagateSolved(provisional: Boolean)(using Context) = + if cs1.isConst && cs2.isConst && !isConst then markSolved(provisional) end Intersection def elemIntersection(cs1: CaptureSet, cs2: CaptureSet)(using Context): Refs = - cs1.elems.filter(cs2.mightAccountFor) ++ cs2.elems.filter(cs1.mightAccountFor) + cs1.elems.filter(cs2.accountsFor) ++ cs2.elems.filter(cs1.accountsFor) + + /** A capture set variable used to record the references hidden by a Fresh instance, + * The elems and deps members are repurposed as follows: + * elems: Set of hidden references + * deps : Set of hidden sets for which the Fresh instance owning this set + * is a hidden element. + * Hidden sets may become aliases of other hidden sets, which means that + * reads and writes of elems go to the alias. + * If H is an alias of R.hidden for some Fresh instance R then: + * H.elems == {R} + * H.deps = {R.hidden} + * This encoding was chosen because it relies only on the elems and deps fields + * which are already subject through snapshotting and rollbacks in VarState. + * It's advantageous if we don't need to deal with other pieces of state there. + */ + class HiddenSet(initialOwner: Symbol)(using @constructorOnly ictx: Context) + extends Var(initialOwner): + var owningCap: AnnotatedType = uninitialized + var givenOwner: Symbol = initialOwner + + override def owner = givenOwner + + // assert(id != 34, i"$initialHidden") + + private def aliasRef: AnnotatedType | Null = + if myElems.size == 1 then + myElems.nth(0) match + case al @ root.Fresh(hidden) if deps.contains(hidden) => al + case _ => null + else null + + private def aliasSet: HiddenSet = + if myElems.size == 1 then + myElems.nth(0) match + case root.Fresh(hidden) if deps.contains(hidden) => hidden + case _ => this + else this + + def superCaps: List[AnnotatedType] = + deps.toList.map(_.asInstanceOf[HiddenSet].owningCap) + + override def elems: Refs = + val al = aliasSet + if al eq this then super.elems else al.elems + + override def elems_=(refs: Refs) = + val al = aliasSet + if al eq this then super.elems_=(refs) else al.elems_=(refs) + + /** Add element to hidden set. Also add it to all supersets (as indicated by + * deps of this set). Follow aliases on both hidden set and added element + * before adding. If the added element is also a Fresh instance with + * hidden set H which is a superset of this set, then make this set an + * alias of H. + */ + def add(elem: CaptureRef)(using ctx: Context, vs: VarState): Unit = + val alias = aliasSet + if alias ne this then alias.add(elem) + else + def addToElems() = + elems += elem + deps.foreach: dep => + assert(dep != this) + vs.addHidden(dep.asInstanceOf[HiddenSet], elem) + elem match + case root.Fresh(hidden) => + if this ne hidden then + val alias = hidden.aliasRef + if alias != null then + add(alias) + else if deps.contains(hidden) then // make this an alias of elem + capt.println(i"Alias $this to $hidden") + elems = SimpleIdentitySet(elem) + deps = SimpleIdentitySet(hidden) + else + addToElems() + hidden.deps += this + case _ => + addToElems() + + /** Apply function `f` to `elems` while setting `elems` to empty for the + * duration. This is used to escape infinite recursions if two Freshs + * refer to each other in their hidden sets. + */ + override def processElems[T](f: Refs => T): T = + val savedElems = elems + elems = emptyRefs + try f(savedElems) + finally elems = savedElems + end HiddenSet /** Extrapolate tm(r) according to `variance`. Let r1 be the result of tm(r). * - If r1 is a tracked CaptureRef, return {r1} @@ -905,25 +1034,32 @@ object CaptureSet: */ def subCapturesRange(arg1: TypeBounds, arg2: Type)(using Context): Boolean = arg1 match case TypeBounds(CapturingType(lo, loRefs), CapturingType(hi, hiRefs)) if lo =:= hi => - given VarState = VarState() + given VarState() val cs2 = arg2.captureSet hiRefs.subCaptures(cs2).isOK && cs2.subCaptures(loRefs).isOK case _ => false - /** A TypeMap with the property that every capture reference in the image - * of the map is mapped to itself. I.e. for all capture references r1, r2, - * if M(r1) == r2 then M(r2) == r2. - */ - trait IdempotentCaptRefMap extends TypeMap - /** A TypeMap that is the identity on capture references */ trait IdentityCaptRefMap extends TypeMap + /** A value of this class is produced and added as a note to ccState + * when a subsumes check decides that an existential variable `ex` cannot be + * instantiated to the other capability `other`. + */ + case class ExistentialSubsumesFailure(val ex: root.Result, val other: CaptureRef) extends ErrorNote + + trait CompareFailure: + private var myErrorNotes: List[ErrorNote] = Nil + def errorNotes: List[ErrorNote] = myErrorNotes + def withNotes(notes: List[ErrorNote]): this.type = + myErrorNotes = notes + this + enum CompareResult extends Showable: case OK - case Fail(trace: List[CaptureSet]) - case LevelError(cs: CaptureSet, elem: CaptureRef) + case Fail(trace: List[CaptureSet]) extends CompareResult, CompareFailure + case LevelError(cs: CaptureSet, elem: CaptureRef) extends CompareResult, CompareFailure, ErrorNote override def toText(printer: Printer): Text = inContext(printer.printerContext): @@ -974,6 +1110,14 @@ object CaptureSet: /** A map from captureset variables to their dependent sets at the time of the snapshot. */ private val depsMap: util.EqHashMap[Var, Deps] = new util.EqHashMap + /** A map from root.Result values to other such values. If two result values + * `a` and `b` are unified, then `eqResultMap(a) = b` and `eqResultMap(b) = a`. + */ + private var eqResultMap: util.SimpleIdentityMap[root.Result, root.Result] = util.SimpleIdentityMap.empty + + /** A snapshot of the `eqResultMap` value at the start of a VarState transaction */ + private var eqResultSnapshot: util.SimpleIdentityMap[root.Result, root.Result] | Null = null + /** The recorded elements of `v` (it's required that a recording was made) */ def elems(v: Var): Refs = elemsMap(v) @@ -981,8 +1125,7 @@ object CaptureSet: def getElems(v: Var): Option[Refs] = elemsMap.get(v) /** Record elements, return whether this was allowed. - * By default, recording is allowed but the special state FrozenState - * overrides this. + * By default, recording is allowed in regular but not in frozen states. */ def putElems(v: Var, elems: Refs): Boolean = { elemsMap(v) = elems; true } @@ -993,58 +1136,158 @@ object CaptureSet: def getDeps(v: Var): Option[Deps] = depsMap.get(v) /** Record dependent sets, return whether this was allowed. - * By default, recording is allowed but the special state FrozenState - * overrides this. + * By default, recording is allowed in regular but not in frozen states. */ def putDeps(v: Var, deps: Deps): Boolean = { depsMap(v) = deps; true } + /** Does this state allow additions of elements to capture set variables? */ + def isOpen = true + def isSeparating = false + + /** Add element to hidden set, recording it in elemsMap, + * return whether this was allowed. By default, recording is allowed + * but the special state VarState.Separate overrides this. + */ + def addHidden(hidden: HiddenSet, elem: CaptureRef)(using Context): Boolean = + elemsMap.get(hidden) match + case None => + elemsMap(hidden) = hidden.elems + depsMap(hidden) = hidden.deps + case _ => + hidden.add(elem)(using ctx, this) + true + + /** If root1 and root2 belong to the same binder but have different originalBinders + * it means that one of the roots was mapped to the binder of the other by a + * substBinder when comparing two method types. In that case we can unify + * the two roots1, provided none of the two roots have already been unified + * themselves. So unification must be 1-1. + */ + def unify(root1: root.Result, root2: root.Result)(using Context): Boolean = + (root1, root2) match + case (root1 @ root.Result(binder1), root2 @ root.Result(binder2)) + if (binder1 eq binder2) + && (root1.rootAnnot.originalBinder ne root2.rootAnnot.originalBinder) + && eqResultMap(root1) == null + && eqResultMap(root2) == null + => + if eqResultSnapshot == null then eqResultSnapshot = eqResultMap + eqResultMap = eqResultMap.updated(root1, root2).updated(root2, root1) + true + case _ => + false + /** Roll back global state to what was recorded in this VarState */ def rollBack(): Unit = elemsMap.keysIterator.foreach(_.resetElems()(using this)) depsMap.keysIterator.foreach(_.resetDeps()(using this)) - end VarState + if eqResultSnapshot != null then eqResultMap = eqResultSnapshot.nn - /** A special state that does not allow to record elements or dependent sets. - * In effect this means that no new elements or dependent sets can be added - * in this state (since the previous state cannot be recorded in a snapshot) - */ - @sharable - object FrozenState extends VarState: - override def putElems(v: Var, refs: Refs) = false - override def putDeps(v: Var, deps: Deps) = false - override def rollBack(): Unit = () - - @sharable - /** A special state that turns off recording of elements. Used only - * in `addSub` to prevent cycles in recordings. - */ - private object UnrecordedState extends VarState: - override def putElems(v: Var, refs: Refs) = true - override def putDeps(v: Var, deps: Deps) = true - override def rollBack(): Unit = () + private var seen: util.EqHashSet[CaptureRef] = new util.EqHashSet + + /** Run test `pred` unless `ref` was seen in an enclosing `ifNotSeen` operation */ + def ifNotSeen(ref: CaptureRef)(pred: => Boolean): Boolean = + if seen.add(ref) then + try pred finally seen -= ref + else false + + override def toString = "open varState" + + object VarState: + + /** A class for states that do not allow to record elements or dependent sets. + * In effect this means that no new elements or dependent sets can be added + * in these states (since the previous state cannot be recorded in a snapshot) + * On the other hand, these states do allow by default Fresh instances to + * subsume arbitary types, which are then recorded in their hidden sets. + */ + class Closed extends VarState: + override def putElems(v: Var, refs: Refs) = false + override def putDeps(v: Var, deps: Deps) = false + override def isOpen = false + override def toString = "closed varState" + + /** A closed state that allows a Fresh instance to subsume a + * reference `r` only if `r` is already present in the hidden set of the instance. + * No new references can be added. + */ + class Separating extends Closed: + override def addHidden(hidden: HiddenSet, elem: CaptureRef)(using Context): Boolean = false + override def toString = "separating varState" + override def isSeparating = true + + /** A closed state that allows a Fresh instance to subsume a + * reference `r` only if `r` is already present in the hidden set of the instance. + * No new references can be added. + */ + def Separate(using Context): Separating = ccState.Separate + + /** Like Separate but in addition we assume that `cap` never subsumes anything else. + * Used in `++` to not lose track of dependencies between function parameters. + */ + def HardSeparate(using Context): Separating = ccState.HardSeparate + + /** A special state that turns off recording of elements. Used only + * in `addSub` to prevent cycles in recordings. Instantiated in ccState.Unrecorded. + */ + class Unrecorded extends VarState: + override def putElems(v: Var, refs: Refs) = true + override def putDeps(v: Var, deps: Deps) = true + override def rollBack(): Unit = () + override def addHidden(hidden: HiddenSet, elem: CaptureRef)(using Context): Boolean = true + override def toString = "unrecorded varState" + + def Unrecorded(using Context): Unrecorded = ccState.Unrecorded + + /** A closed state that turns off recording of hidden elements (but allows + * adding them). Used in `mightAccountFor`. Instantiated in ccState.ClosedUnrecorded. + */ + class ClosedUnrecorded extends Closed: + override def addHidden(hidden: HiddenSet, elem: CaptureRef)(using Context): Boolean = true + override def toString = "closed unrecorded varState" + + def ClosedUnrecorded(using Context): ClosedUnrecorded = ccState.ClosedUnrecorded + + end VarState /** The current VarState, as passed by the implicit context */ def varState(using state: VarState): VarState = state - /** Maps `x` to `x?` */ - private class MaybeMap(using Context) extends BiTypeMap: + /** A template for maps on capabilities where f(c) <: c and f(f(c)) = c */ + private abstract class NarrowingCapabilityMap(using Context) extends BiTypeMap: + + def mapRef(ref: CaptureRef): CaptureRef def apply(t: Type) = t match - case t: CaptureRef if t.isTrackableRef => t.maybe + case t: CaptureRef if t.isTrackableRef => mapRef(t) case _ => mapOver(t) - override def toString = "Maybe" + override def fuse(next: BiTypeMap)(using Context) = next match + case next: Inverse if next.inverse.getClass == getClass => assert(false); Some(IdentityTypeMap) + case next: NarrowingCapabilityMap if next.getClass == getClass => assert(false) + case _ => None - lazy val inverse = new BiTypeMap: + class Inverse extends BiTypeMap: + def apply(t: Type) = t // since f(c) <: c, this is the best inverse + def inverse = NarrowingCapabilityMap.this + override def toString = NarrowingCapabilityMap.this.toString ++ ".inverse" + override def fuse(next: BiTypeMap)(using Context) = next match + case next: NarrowingCapabilityMap if next.inverse.getClass == getClass => assert(false); Some(IdentityTypeMap) + case next: NarrowingCapabilityMap if next.getClass == getClass => assert(false) + case _ => None - def apply(t: Type) = t match - case t: CaptureRef if t.isMaybe => t.stripMaybe - case t => mapOver(t) + lazy val inverse = Inverse() + end NarrowingCapabilityMap - def inverse = MaybeMap.this + /** Maps `x` to `x?` */ + private class MaybeMap(using Context) extends NarrowingCapabilityMap: + def mapRef(ref: CaptureRef): CaptureRef = ref.maybe + override def toString = "Maybe" - override def toString = "Maybe.inverse" - end MaybeMap + /** Maps `x` to `x.rd` */ + private class ReadOnlyMap(using Context) extends NarrowingCapabilityMap: + def mapRef(ref: CaptureRef): CaptureRef = ref.readOnly + override def toString = "ReadOnly" /* Not needed: def ofClass(cinfo: ClassInfo, argTypes: List[Type])(using Context): CaptureSet = @@ -1073,11 +1316,23 @@ object CaptureSet: case ReachCapability(ref1) => ref1.widen.deepCaptureSet(includeTypevars = true) .showing(i"Deep capture set of $ref: ${ref1.widen} = ${result}", capt) + case ReadOnlyCapability(ref1) => + ref1.captureSetOfInfo.map(ReadOnlyMap()) + case ref: ParamRef if !ref.underlying.exists => + // might happen during construction of lambdas, assume `{cap}` in this case so that + // `ref` will not seem subsumed by other capabilities in a `++`. + universal case _ => - if ref.isMaxCapability then ref.singletonCaptureSet - else ofType(ref.underlying, followResult = true) - - /** Capture set of a type */ + if ref.isRootCapability then ref.singletonCaptureSet + else ofType(ref.underlying, followResult = false) + + /** Capture set of a type + * @param followResult If true, also include capture sets of function results. + * This mode is currently not used. It could be interesting + * when we change the system so that the capture set of a function + * is the union of the capture sets if its span. + * In this case we should use `followResult = true` in the call in ofInfo above. + */ def ofType(tp: Type, followResult: Boolean)(using Context): CaptureSet = def recur(tp: Type): CaptureSet = trace(i"ofType $tp, ${tp.getClass} $followResult", show = true): tp.dealiasKeepAnnots match @@ -1088,9 +1343,14 @@ object CaptureSet: case tp: (TypeRef | TypeParamRef) => if tp.derivesFrom(defn.Caps_CapSet) then tp.captureSet else empty + case tp @ root.Result(_) => + tp.captureSet case CapturingType(parent, refs) => recur(parent) ++ refs case tp @ AnnotatedType(parent, ann) if ann.hasSymbol(defn.ReachCapabilityAnnot) => + // Note: we don't use the `ReachCapability(parent)` extractor here since that + // only works if `parent` is a CaptureRef, but in illegal programs it might not be. + // And then we do not want to fall back to empty. parent match case parent: SingletonCaptureRef if parent.isTrackableRef => tp.singletonCaptureSet @@ -1098,8 +1358,11 @@ object CaptureSet: CaptureSet.ofTypeDeeply(parent.widen) case tpd @ defn.RefinedFunctionOf(rinfo: MethodType) if followResult => ofType(tpd.parent, followResult = false) // pick up capture set from parent type - ++ (recur(rinfo.resType) // add capture set of result - -- CaptureSet(rinfo.paramRefs.filter(_.isTracked)*)) // but disregard bound parameters + ++ recur(rinfo.resType) // add capture set of result + .filter: + case TermParamRef(binder, _) => binder ne rinfo + case root.Result(binder) => binder ne rinfo + case _ => true case tpd @ AppliedType(tycon, args) => if followResult && defn.isNonRefinedFunction(tpd) then recur(args.last) @@ -1123,33 +1386,14 @@ object CaptureSet: /** The deep capture set of a type is the union of all covariant occurrences of * capture sets. Nested existential sets are approximated with `cap`. - * NOTE: The traversal logic needs to be in sync with narrowCaps in CaptureOps, which - * replaces caps with reach capabilties. The one exception to this is invariant - * arguments. This have to be included to be conservative in dcs but must be - * excluded in narrowCaps. */ def ofTypeDeeply(tp: Type, includeTypevars: Boolean = false)(using Context): CaptureSet = - val collect = new TypeAccumulator[CaptureSet]: - val seen = util.HashSet[Symbol]() - def apply(cs: CaptureSet, t: Type) = - if variance < 0 then cs - else t.dealias match - case t @ CapturingType(p, cs1) => - this(cs, p) ++ cs1 - case t @ AnnotatedType(parent, ann) => - this(cs, parent) - case t: TypeRef if t.symbol.isAbstractOrParamType && !seen.contains(t.symbol) => - seen += t.symbol - val upper = t.info.bounds.hi - if includeTypevars && upper.isExactlyAny then CaptureSet.universal - else this(cs, upper) - case t @ FunctionOrMethod(args, res @ Existential(_, _)) - if args.forall(_.isAlwaysPure) => - this(cs, Existential.toCap(res)) - case t @ Existential(_, _) => - cs - case _ => - foldOver(cs, t) + val collect = new DeepTypeAccumulator[CaptureSet]: + def capturingCase(acc: CaptureSet, parent: Type, refs: CaptureSet) = + this(acc, parent) ++ refs + def abstractTypeCase(acc: CaptureSet, t: TypeRef, upperBound: Type) = + if includeTypevars && upperBound.isExactlyAny then CaptureSet.fresh(t.symbol) + else this(acc, upperBound) collect(CaptureSet.empty, tp) type AssumedContains = immutable.Map[TypeRef, SimpleIdentitySet[CaptureRef]] @@ -1190,23 +1434,4 @@ object CaptureSet: println(i" ${cv.show.padTo(20, ' ')} :: ${cv.deps.toList}%, %") } else op - - def levelErrors: Addenda = new Addenda: - override def toAdd(using Context) = - for CompareResult.LevelError(cs, ref) <- ccState.levelError.toList yield - ccState.levelError = None - if ref.isRootCapability then - i""" - | - |Note that the universal capability `cap` - |cannot be included in capture set $cs""" - else - val levelStr = ref match - case ref: TermRef => i", defined in ${ref.symbol.maybeOwner}" - case _ => "" - i""" - | - |Note that reference ${ref}$levelStr - |cannot be included in outer capture set $cs""" - end CaptureSet diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 830d9ad0a4d4..a4551fa2b86c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -18,11 +18,12 @@ import util.{SimpleIdentitySet, EqHashMap, EqHashSet, SrcPos, Property} import transform.{Recheck, PreRecheck, CapturedVars} import Recheck.* import scala.collection.mutable -import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult} +import CaptureSet.{withCaptureSetsExplained, CompareResult, CompareFailure, ExistentialSubsumesFailure} import CCState.* import StdNames.nme import NameKinds.{DefaultGetterName, WildcardParamName, UniqueNameKind} import reporting.{trace, Message, OverrideError} +import Annotations.Annotation /** The capture checker */ object CheckCaptures: @@ -58,7 +59,7 @@ object CheckCaptures: def isOutermost = outer0 == null /** If an environment is open it tracks free references */ - def isOpen = !captured.isAlwaysEmpty && kind != EnvKind.Boxed + def isOpen(using Context) = !captured.isAlwaysEmpty && kind != EnvKind.Boxed def outersIterator: Iterator[Env] = new: private var cur = Env.this @@ -76,7 +77,7 @@ object CheckCaptures: * maps parameters in contravariant capture sets to the empty set. */ final class SubstParamsMap(from: BindingType, to: List[Type])(using Context) - extends ApproximatingTypeMap, IdempotentCaptRefMap: + extends ApproximatingTypeMap: def apply(tp: Type): Type = tp match case tp: ParamRef => @@ -88,44 +89,9 @@ object CheckCaptures: tp case _ => mapOver(tp) + override def toString = "SubstParamsMap" end SubstParamsMap - /** Used for substituting parameters in a special case: when all actual arguments - * are mutually distinct capabilities. - */ - final class SubstParamsBiMap(from: LambdaType, to: List[Type])(using Context) - extends BiTypeMap: - thisMap => - - def apply(tp: Type): Type = tp match - case tp: ParamRef => - if tp.binder == from then to(tp.paramNum) else tp - case tp: NamedType => - if tp.prefix `eq` NoPrefix then tp - else tp.derivedSelect(apply(tp.prefix)) - case _: ThisType => - tp - case _ => - mapOver(tp) - - lazy val inverse = new BiTypeMap: - def apply(tp: Type): Type = tp match - case tp: NamedType => - var idx = 0 - var to1 = to - while idx < to.length && (tp ne to(idx)) do - idx += 1 - to1 = to1.tail - if idx < to.length then from.paramRefs(idx) - else if tp.prefix `eq` NoPrefix then tp - else tp.derivedSelect(apply(tp.prefix)) - case _: ThisType => - tp - case _ => - mapOver(tp) - def inverse = thisMap - end SubstParamsBiMap - /** A prototype that indicates selection with an immutable value */ class PathSelectionProto(val sym: Symbol, val pt: Type)(using Context) extends WildcardSelectionProto @@ -150,6 +116,7 @@ object CheckCaptures: |is must be a type parameter or abstract type with a caps.CapSet upper bound.""", elem.srcPos) case ReachCapabilityApply(arg) => check(arg, elem.srcPos) + case ReadOnlyCapabilityApply(arg) => check(arg, elem.srcPos) case _ => check(elem, elem.srcPos) /** Under the sealed policy, report an error if some part of `tp` contains the @@ -161,6 +128,10 @@ object CheckCaptures: private val seen = new EqHashSet[TypeRef] + // We keep track of open existential scopes here so that we can set these scopes + // in ccState when printing a part of the offending type. + var openExistentialScopes: List[MethodType] = Nil + def traverse(t: Type) = t.dealiasKeepAnnots match case t: TypeRef => @@ -178,54 +149,40 @@ object CheckCaptures: () case CapturingType(parent, refs) => if variance >= 0 then + val openScopes = openExistentialScopes refs.disallowRootCapability: () => - def part = if t eq tp then "" else i"the part $t of " + def part = + if t eq tp then "" + else + // Show in context of all enclosing traversed existential scopes. + def showInOpenedFreshBinders(mts: List[MethodType]): String = mts match + case Nil => i"the part $t of " + case mt :: mts1 => + CCState.inNewExistentialScope(mt): + showInOpenedFreshBinders(mts1) + showInOpenedFreshBinders(openScopes.reverse) report.error( em"""$what cannot $have $tp since |${part}that type captures the root capability `cap`.$addendum""", pos) traverse(parent) + case defn.RefinedFunctionOf(mt) => + traverse(mt) + case t: MethodType if t.marksExistentialScope => + atVariance(-variance): + t.paramInfos.foreach(traverse) + val saved = openExistentialScopes + openExistentialScopes = t :: openExistentialScopes + try traverse(t.resType) + finally openExistentialScopes = saved case t => traverseChildren(t) - if ccConfig.useSealed then check.traverse(tp) + check.traverse(tp) end disallowRootCapabilitiesIn - /** If we are not under the sealed policy, and a tree is an application that unboxes - * its result or is a try, check that the tree's type does not have covariant universal - * capabilities. - */ - private def checkNotUniversalInUnboxedResult(tpe: Type, tree: Tree)(using Context): Unit = - def needsUniversalCheck = tree match - case _: RefTree | _: Apply | _: TypeApply => tree.symbol.unboxesResult - case _: Try => true - case _ => false - - object checkNotUniversal extends TypeTraverser: - def traverse(tp: Type) = - tp.dealias match - case wtp @ CapturingType(parent, refs) => - if variance > 0 then - refs.disallowRootCapability: () => - def part = if wtp eq tpe.widen then "" else i" in its part $wtp" - report.error( - em"""The expression's type ${tpe.widen} is not allowed to capture the root capability `cap`$part. - |This usually means that a capability persists longer than its allowed lifetime.""", - tree.srcPos) - if !wtp.isBoxed then traverse(parent) - case tp => - traverseChildren(tp) - - if !ccConfig.useSealed - && !tpe.hasAnnotation(defn.UncheckedCapturesAnnot) - && needsUniversalCheck - && tpe.widen.isValueType - then - checkNotUniversal.traverse(tpe.widen) - end checkNotUniversalInUnboxedResult - trait CheckerAPI: /** Complete symbol info of a val or a def */ - def completeDef(tree: ValOrDefDef, sym: Symbol)(using Context): Type + def completeDef(tree: ValOrDefDef, sym: Symbol, completer: LazyType)(using Context): Type extension [T <: Tree](tree: T) @@ -237,6 +194,21 @@ object CheckCaptures: /** Was a new type installed for this tree? */ def hasNuType: Boolean + + /** Is this tree passed to a parameter or assigned to a value with a type + * that contains cap in no-flip covariant position, which will necessite + * a separation check? + */ + def needsSepCheck: Boolean + + /** If a tree is an argument for which needsSepCheck is true, + * the type of the formal paremeter corresponding to the argument. + */ + def formalType: Type + + /** The "use set", i.e. the capture set marked as free at this node. */ + def markedFree: CaptureSet + end CheckerAPI class CheckCaptures extends Recheck, SymTransformer: @@ -261,6 +233,8 @@ class CheckCaptures extends Recheck, SymTransformer: class CaptureChecker(ictx: Context) extends Rechecker(ictx), CheckerAPI: + // println(i"checking ${ictx.source}"(using ictx)) + /** The current environment */ private val rootEnv: Env = inContext(ictx): Env(defn.RootClass, EnvKind.Regular, CaptureSet.empty, null) @@ -277,61 +251,147 @@ class CheckCaptures extends Recheck, SymTransformer: */ private val todoAtPostCheck = new mutable.ListBuffer[() => Unit] + /** Maps trees that need a separation check because they are arguments to + * polymorphic parameters. The trees are mapped to the formal parameter type. + */ + private val sepCheckFormals = util.EqHashMap[Tree, Type]() + + /** The references used at identifier or application trees */ + private val usedSet = util.EqHashMap[Tree, CaptureSet]() + + /** The set of symbols that were rechecked via a completer */ + private val completed = new mutable.HashSet[Symbol] + + var needAnotherRun = false + + def resetIteration()(using Context): Unit = + needAnotherRun = false + resetNuTypes() + todoAtPostCheck.clear() + completed.clear() + + extension [T <: Tree](tree: T) + def needsSepCheck: Boolean = sepCheckFormals.contains(tree) + def formalType: Type = sepCheckFormals.getOrElse(tree, NoType) + def markedFree = usedSet.getOrElse(tree, CaptureSet.empty) + /** Instantiate capture set variables appearing contra-variantly to their * upper approximation. */ - private def interpolator(startingVariance: Int = 1)(using Context) = new TypeTraverser: - variance = startingVariance - override def traverse(t: Type) = t match - case t @ CapturingType(parent, refs) => - refs match - case refs: CaptureSet.Var if variance < 0 => refs.solve() + private def interpolate(tp: Type, sym: Symbol, startingVariance: Int = 1)(using Context): Unit = + + object variances extends TypeTraverser: + variance = startingVariance + val varianceOfVar = EqHashMap[CaptureSet.Var, Int]() + override def traverse(t: Type) = t match + case t @ CapturingType(parent, refs) => + refs match + case refs: CaptureSet.Var if !refs.isConst => + varianceOfVar(refs) = varianceOfVar.get(refs) match + case Some(v0) => if v0 == 0 then 0 else (v0 + variance) / 2 + case None => variance + case _ => + traverse(parent) + case t @ defn.RefinedFunctionOf(rinfo) => + traverse(rinfo) + case _ => + traverseChildren(t) + + val interpolator = new TypeTraverser: + override def traverse(t: Type) = t match + case t @ CapturingType(parent, refs) => + refs match + case refs: CaptureSet.Var if !refs.isConst => + if variances.varianceOfVar(refs) < 0 then refs.solve() + else refs.markSolved(provisional = !sym.isMutableVar) + case _ => + traverse(parent) + case t @ defn.RefinedFunctionOf(rinfo) => + traverse(rinfo) + case _ => + traverseChildren(t) + + variances.traverse(tp) + interpolator.traverse(tp) + end interpolate + + /* Also set any previously unset owners of toplevel Fresh instances to improve + * error diagnostics in separation checking. + */ + private def anchorCaps(sym: Symbol)(using Context) = new TypeTraverser: + override def traverse(t: Type) = + if variance > 0 then + t match + case t @ CapturingType(parent, refs) => + for ref <- refs.elems do + ref match + case root.Fresh(hidden) if !hidden.givenOwner.exists => + hidden.givenOwner = sym + case _ => + traverse(parent) + case t @ defn.RefinedFunctionOf(rinfo) => + traverse(rinfo) case _ => - traverse(parent) - case t @ defn.RefinedFunctionOf(rinfo) => - traverse(rinfo) - case _ => - traverseChildren(t) + traverseChildren(t) /** If `tpt` is an inferred type, interpolate capture set variables appearing contra- - * variantly in it. + * variantly in it. Also anchor Fresh instances with anchorCaps. */ - private def interpolateVarsIn(tpt: Tree)(using Context): Unit = + private def interpolateIfInferred(tpt: Tree, sym: Symbol)(using Context): Unit = if tpt.isInstanceOf[InferredTypeTree] then - interpolator().traverse(tpt.nuType) - .showing(i"solved vars in ${tpt.nuType}", capt) - for msg <- ccState.approxWarnings do - report.warning(msg, tpt.srcPos) - ccState.approxWarnings.clear() + interpolate(tpt.nuType, sym) + .showing(i"solved vars for $sym in ${tpt.nuType}", capt) + anchorCaps(sym).traverse(tpt.nuType) + for msg <- ccState.approxWarnings do + report.warning(msg, tpt.srcPos) + ccState.approxWarnings.clear() /** Assert subcapturing `cs1 <: cs2` (available for debugging, otherwise unused) */ def assertSub(cs1: CaptureSet, cs2: CaptureSet)(using Context) = - assert(cs1.subCaptures(cs2, frozen = false).isOK, i"$cs1 is not a subset of $cs2") + assert(cs1.subCaptures(cs2).isOK, i"$cs1 is not a subset of $cs2") /** If `res` is not CompareResult.OK, report an error */ - def checkOK(res: CompareResult, prefix: => String, pos: SrcPos, provenance: => String = "")(using Context): Unit = - if !res.isOK then - def toAdd: String = CaptureSet.levelErrors.toAdd.mkString - def descr: String = - val d = res.blocking.description - if d.isEmpty then provenance else "" - report.error(em"$prefix included in the allowed capture set ${res.blocking}$descr$toAdd", pos) + def checkOK(res: CompareResult, prefix: => String, added: CaptureRef | CaptureSet, target: CaptureSet, pos: SrcPos, provenance: => String = "")(using Context): Unit = + res match + case res: CompareFailure => + def msg(provisional: Boolean) = + def toAdd: String = errorNotes(res.errorNotes).toAdd.mkString + def descr: String = + val d = res.blocking.description + if d.isEmpty then provenance else "" + def kind = if provisional then "previously estimated\n" else "allowed " + em"$prefix included in the ${kind}capture set ${res.blocking}$descr$toAdd" + target match + case target: CaptureSet.Var + if res.blocking.isProvisionallySolved => + report.warning( + msg(provisional = true) + .prepend(i"Another capture checking run needs to be scheduled because\n"), + pos) + needAnotherRun = true + added match + case added: CaptureRef => target.elems += added + case added: CaptureSet => target.elems ++= added.elems + case _ => + inContext(root.printContext(added, res.blocking)): + report.error(msg(provisional = false), pos) + case _ => /** Check subcapturing `{elem} <: cs`, report error on failure */ def checkElem(elem: CaptureRef, cs: CaptureSet, pos: SrcPos, provenance: => String = "")(using Context) = checkOK( - elem.singletonCaptureSet.subCaptures(cs, frozen = false), + ccState.test(elem.singletonCaptureSet.subCaptures(cs)), i"$elem cannot be referenced here; it is not", - pos, provenance) + elem, cs, pos, provenance) /** Check subcapturing `cs1 <: cs2`, report error on failure */ def checkSubset(cs1: CaptureSet, cs2: CaptureSet, pos: SrcPos, provenance: => String = "", cs1description: String = "")(using Context) = checkOK( - cs1.subCaptures(cs2, frozen = false), + ccState.test(cs1.subCaptures(cs2)), if cs1.elems.size == 1 then i"reference ${cs1.elems.toList.head}$cs1description is not" else i"references $cs1$cs1description are not all", - pos, provenance) + cs1, cs2, pos, provenance) /** If `sym` is a class or method nested inside a term, a capture set variable representing * the captured variables of the environment associated with `sym`. @@ -339,7 +399,7 @@ class CheckCaptures extends Recheck, SymTransformer: def capturedVars(sym: Symbol)(using Context): CaptureSet = myCapturedVars.getOrElseUpdate(sym, if sym.ownersIterator.exists(_.isTerm) - then CaptureSet.Var(sym.owner, level = sym.ccLevel) + then CaptureSet.Var(sym.owner, level = ccState.symLevel(sym)) else CaptureSet.empty) // ---- Record Uses with MarkFree ---------------------------------------------------- @@ -367,7 +427,7 @@ class CheckCaptures extends Recheck, SymTransformer: i"\nof the enclosing ${owner.showLocated}" /** Does the given environment belong to a method that is (a) nested in a term - * and (b) not the method of an anonympus function? + * and (b) not the method of an anonymous function? */ def isOfNestedMethod(env: Env | Null)(using Context) = env != null @@ -378,17 +438,17 @@ class CheckCaptures extends Recheck, SymTransformer: /** Include `sym` in the capture sets of all enclosing environments nested in the * the environment in which `sym` is defined. */ - def markFree(sym: Symbol, pos: SrcPos)(using Context): Unit = - markFree(sym, sym.termRef, pos) + def markFree(sym: Symbol, tree: Tree)(using Context): Unit = + markFree(sym, sym.termRef, tree) - def markFree(sym: Symbol, ref: TermRef, pos: SrcPos)(using Context): Unit = - if sym.exists && ref.isTracked then markFree(ref.captureSet, pos) + def markFree(sym: Symbol, ref: CaptureRef, tree: Tree)(using Context): Unit = + if sym.exists && ref.isTracked then markFree(ref.captureSet, tree) /** Make sure the (projected) `cs` is a subset of the capture sets of all enclosing * environments. At each stage, only include references from `cs` that are outside * the environment's owner */ - def markFree(cs: CaptureSet, pos: SrcPos)(using Context): Unit = + def markFree(cs: CaptureSet, tree: Tree)(using Context): Unit = // A captured reference with the symbol `sym` is visible from the environment // if `sym` is not defined inside the owner of the environment. inline def isVisibleFromEnv(sym: Symbol, env: Env) = @@ -410,7 +470,7 @@ class CheckCaptures extends Recheck, SymTransformer: val what = if ref.isType then "Capture set parameter" else "Local reach capability" report.error( em"""$what $c leaks into capture scope of ${env.ownerString}. - |To allow this, the ${ref.symbol} should be declared with a @use annotation""", pos) + |To allow this, the ${ref.symbol} should be declared with a @use annotation""", tree.srcPos) case _ => /** Avoid locally defined capability by charging the underlying type @@ -430,7 +490,7 @@ class CheckCaptures extends Recheck, SymTransformer: CaptureSet.ofType(c.widen, followResult = false) capt.println(i"Widen reach $c to $underlying in ${env.owner}") underlying.disallowRootCapability: () => - report.error(em"Local capability $c in ${env.ownerString} cannot have `cap` as underlying capture set", pos) + report.error(em"Local capability $c in ${env.ownerString} cannot have `cap` as underlying capture set", tree.srcPos) recur(underlying, env, lastEnv) /** Avoid locally defined capability if it is a reach capability or capture set @@ -452,9 +512,15 @@ class CheckCaptures extends Recheck, SymTransformer: // The path-use.scala neg test contains an example. val underlying = CaptureSet.ofTypeDeeply(c1.widen) capt.println(i"Widen reach $c to $underlying in ${env.owner}") - underlying.disallowRootCapability: () => - report.error(em"Local reach capability $c leaks into capture scope of ${env.ownerString}", pos) - recur(underlying, env, null) + if ccConfig.useSepChecks then + recur(underlying.filter(!_.isRootCapability), env, null) + // we don't want to disallow underlying Fresh instances, since these are typically locally created + // fresh capabilities. We don't need to also follow the hidden set since separation + // checking makes ure that locally hidden references need to go to @consume parameters. + else + underlying.disallowRootCapability: () => + report.error(em"Local reach capability $c leaks into capture scope of ${env.ownerString}", tree.srcPos) + recur(underlying, env, null) case c: TypeRef if c.isParamPath => checkUseDeclared(c, env, null) case _ => @@ -470,7 +536,7 @@ class CheckCaptures extends Recheck, SymTransformer: then avoidLocalCapability(c, env, lastEnv) else avoidLocalReachCapability(c, env) isVisible - checkSubset(included, env.captured, pos, provenance(env)) + checkSubset(included, env.captured, tree.srcPos, provenance(env)) capt.println(i"Include call or box capture $included from $cs in ${env.owner} --> ${env.captured}") if !isOfNestedMethod(env) then recur(included, nextEnvToCharge(env, !_.owner.isStaticOwner), env) @@ -478,13 +544,18 @@ class CheckCaptures extends Recheck, SymTransformer: // will be charged when that method is called. recur(cs, curEnv, null) + usedSet(tree) = tree.markedFree ++ cs end markFree /** Include references captured by the called method in the current environment stack */ - def includeCallCaptures(sym: Symbol, resType: Type, pos: SrcPos)(using Context): Unit = resType match + def includeCallCaptures(sym: Symbol, resType: Type, tree: Tree)(using Context): Unit = resType match case _: MethodOrPoly => // wait until method is fully applied case _ => - if sym.exists && curEnv.isOpen then markFree(capturedVars(sym), pos) + def isRetained(ref: CaptureRef): Boolean = ref.pathRoot match + case root: ThisType => ctx.owner.isContainedIn(root.cls) + case _ => true + if sym.exists && curEnv.isOpen then + markFree(capturedVars(sym).filter(isRetained), tree) /** Under the sealed policy, disallow the root capability in type arguments. * Type arguments come either from a TypeApply node or from an AppliedType @@ -497,7 +568,7 @@ class CheckCaptures extends Recheck, SymTransformer: */ def disallowCapInTypeArgs(fn: Tree, sym: Symbol, args: List[Tree])(using Context): Unit = def isExempt = sym.isTypeTestOrCast || sym == defn.Compiletime_erasedValue - if ccConfig.useSealed && !isExempt then + if !isExempt then val paramNames = atPhase(thisPhase.prev): fn.tpe.widenDealias match case tl: TypeLambda => tl.paramNames @@ -508,44 +579,57 @@ class CheckCaptures extends Recheck, SymTransformer: for case (arg: TypeTree, pname) <- args.lazyZip(paramNames) do def where = if sym.exists then i" in an argument of $sym" else "" - val (addendum, pos) = + val (addendum, errTree) = if arg.isInferred - then ("\nThis is often caused by a local capability$where\nleaking as part of its result.", fn.srcPos) - else if arg.span.exists then ("", arg.srcPos) - else ("", fn.srcPos) + then ("\nThis is often caused by a local capability$where\nleaking as part of its result.", fn) + else if arg.span.exists then ("", arg) + else ("", fn) disallowRootCapabilitiesIn(arg.nuType, NoSymbol, - i"Type variable $pname of $sym", "be instantiated to", addendum, pos) + i"Type variable $pname of $sym", "be instantiated to", addendum, errTree.srcPos) val param = fn.symbol.paramNamed(pname) - if param.isUseParam then markFree(arg.nuType.deepCaptureSet, pos) + if param.isUseParam then markFree(arg.nuType.deepCaptureSet, errTree) end disallowCapInTypeArgs + /** Rechecking idents involves: + * - adding call captures for idents referring to methods + * - marking as free the identifier with any selections or .rd + * modifiers implied by the expected type + */ override def recheckIdent(tree: Ident, pt: Type)(using Context): Type = val sym = tree.symbol if sym.is(Method) then // If ident refers to a parameterless method, charge its cv to the environment - includeCallCaptures(sym, sym.info, tree.srcPos) + includeCallCaptures(sym, sym.info, tree) else if !sym.isStatic then - // Otherwise charge its symbol, but add all selections implied by the e - // expected type `pt`. - // Example: If we have `x` and the expected type says we select that with `.a.b`, - // we charge `x.a.b` instead of `x`. - def addSelects(ref: TermRef, pt: Type): TermRef = pt match + // Otherwise charge its symbol, but add all selections and also any `.rd` + // modifier implied by the expected type `pt`. + // Example: If we have `x` and the expected type says we select that with `.a.b` + // where `b` is a read-only method, we charge `x.a.b.rd` instead of `x`. + def addSelects(ref: TermRef, pt: Type): CaptureRef = pt match case pt: PathSelectionProto if ref.isTracked => - // if `ref` is not tracked then the selection could not give anything new - // class SerializationProxy in stdlib-cc/../LazyListIterable.scala has an example where this matters. - addSelects(ref.select(pt.sym).asInstanceOf[TermRef], pt.pt) + if pt.sym.isReadOnlyMethod then + ref.readOnly + else + // if `ref` is not tracked then the selection could not give anything new + // class SerializationProxy in stdlib-cc/../LazyListIterable.scala has an example where this matters. + addSelects(ref.select(pt.sym).asInstanceOf[TermRef], pt.pt) case _ => ref - val pathRef = addSelects(sym.termRef, pt) - markFree(sym, pathRef, tree.srcPos) + var pathRef: CaptureRef = addSelects(sym.termRef, pt) + if pathRef.derivesFrom(defn.Caps_Mutable) && pt.isValueType && !pt.isMutableType then + pathRef = pathRef.readOnly + markFree(sym, pathRef, tree) super.recheckIdent(tree, pt) /** The expected type for the qualifier of a selection. If the selection - * could be part of a capabaility path, we return a PathSelectionProto. + * could be part of a capability path or is a a read-only method, we return + * a PathSelectionProto. */ override def selectionProto(tree: Select, pt: Type)(using Context): Type = val sym = tree.symbol - if !sym.isOneOf(UnstableValueFlags) && !sym.isStatic then PathSelectionProto(sym, pt) + if !sym.isOneOf(UnstableValueFlags) && !sym.isStatic + || sym.isReadOnlyMethod + then PathSelectionProto(sym, pt) else super.selectionProto(tree, pt) /** A specialized implementation of the selection rule. @@ -573,6 +657,15 @@ class CheckCaptures extends Recheck, SymTransformer: } case _ => denot + // Don't allow update methods to be called unless the qualifier captures + // an exclusive reference. TODO This should probably rolled into + // qualifier logic once we have it. + if tree.symbol.isUpdateMethod && !qualType.captureSet.isExclusive then + report.error( + em"""cannot call update ${tree.symbol} from $qualType, + |since its capture set ${qualType.captureSet} is read-only""", + tree.srcPos) + val selType = recheckSelection(tree, qualType, name, disambiguate) val selWiden = selType.widen @@ -580,7 +673,7 @@ class CheckCaptures extends Recheck, SymTransformer: // - on the LHS of assignments, or // - if the qualifier or selection type is boxed, or // - the selection is either a trackable capture ref or a pure type - if pt == LhsProto + if noWiden(selType, pt) || qualType.isBoxedCapturing || selWiden.isBoxedCapturing || selType.isTrackableRef @@ -602,15 +695,17 @@ class CheckCaptures extends Recheck, SymTransformer: selType }//.showing(i"recheck sel $tree, $qualType = $result") - /** Hook for massaging a function before it is applied. Copies all @use annotations - * on method parameter symbols to the corresponding paramInfo types. + /** Hook for massaging a function before it is applied. Copies all @use and @consume + * annotations on method parameter symbols to the corresponding paramInfo types. */ override def prepareFunction(funtpe: MethodType, meth: Symbol)(using Context): MethodType = - val paramInfosWithUses = funtpe.paramInfos.zipWithConserve(funtpe.paramNames): (formal, pname) => - val param = meth.paramNamed(pname) - param.getAnnotation(defn.UseAnnot) match - case Some(ann) => AnnotatedType(formal, ann) - case _ => formal + val paramInfosWithUses = + funtpe.paramInfos.zipWithConserve(funtpe.paramNames): (formal, pname) => + val param = meth.paramNamed(pname) + def copyAnnot(tp: Type, cls: ClassSymbol) = param.getAnnotation(cls) match + case Some(ann) => AnnotatedType(tp, ann) + case _ => tp + copyAnnot(copyAnnot(formal, defn.UseAnnot), defn.ConsumeAnnot) funtpe.derivedLambdaType(paramInfos = paramInfosWithUses) /** Recheck applications, with special handling of unsafeAssumePure. @@ -620,28 +715,31 @@ class CheckCaptures extends Recheck, SymTransformer: val meth = tree.fun.symbol if meth == defn.Caps_unsafeAssumePure then val arg :: Nil = tree.args: @unchecked - val argType0 = recheck(arg, pt.capturing(CaptureSet.universal)) + val argType0 = recheck(arg, pt.stripCapturing.capturing(root.Fresh())) val argType = if argType0.captureSet.isAlwaysEmpty then argType0 else argType0.widen.stripCapturing - capt.println(i"rechecking $arg with $pt: $argType") + capt.println(i"rechecking unsafeAssumePure of $arg with $pt: $argType") super.recheckFinish(argType, tree, pt) else val res = super.recheckApply(tree, pt) - includeCallCaptures(meth, res, tree.srcPos) + includeCallCaptures(meth, res, tree) res - /** Recheck argument, and, if formal parameter carries a `@use`, + /** Recheck argument against a "freshened" version of `formal` where toplevel `cap` + * occurrences are replaced by `Fresh` instances. Also, if formal parameter carries a `@use`, * charge the deep capture set of the actual argument to the environment. */ protected override def recheckArg(arg: Tree, formal: Type)(using Context): Type = - val argType = recheck(arg, formal) - formal match - case AnnotatedType(formal1, ann) if ann.symbol == defn.UseAnnot => - // The UseAnnot is added to `formal` by `prepareFunction` - capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}") - markFree(argType.deepCaptureSet, arg.srcPos) - case _ => + val freshenedFormal = root.capToFresh(formal) + val argType = recheck(arg, freshenedFormal) + .showing(i"recheck arg $arg vs $freshenedFormal = $result", capt) + if formal.hasAnnotation(defn.UseAnnot) || formal.hasAnnotation(defn.ConsumeAnnot) then + // The @use and/or @consume annotation is added to `formal` by `prepareFunction` + capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}") + markFree(argType.deepCaptureSet, arg) + if formal.containsCap then + sepCheckFormals(arg) = freshenedFormal argType /** Map existential captures in result to `cap` and implement the following @@ -667,13 +765,11 @@ class CheckCaptures extends Recheck, SymTransformer: */ protected override def recheckApplication(tree: Apply, qualType: Type, funType: MethodType, argTypes: List[Type])(using Context): Type = - val appType = Existential.toCap(super.recheckApplication(tree, qualType, funType, argTypes)) + val appType = root.resultToFresh(super.recheckApplication(tree, qualType, funType, argTypes)) val qualCaptures = qualType.captureSet val argCaptures = for (argType, formal) <- argTypes.lazyZip(funType.paramInfos) yield - formal match - case AnnotatedType(_, ann) if ann.symbol == defn.UseAnnot => argType.deepCaptureSet - case _ => argType.captureSet + if formal.hasAnnotation(defn.UseAnnot) then argType.deepCaptureSet else argType.captureSet appType match case appType @ CapturingType(appType1, refs) if qualType.exists @@ -694,19 +790,11 @@ class CheckCaptures extends Recheck, SymTransformer: * This means * - Instantiate result type with actual arguments * - if `sym` is a constructor, refine its type with `refineInstanceType` - * If all argument types are mutually different trackable capture references, use a BiTypeMap, - * since that is more precise. Otherwise use a normal idempotent map, which might lose information - * in the case where the result type contains captureset variables that are further - * constrained afterwards. */ override def instantiate(mt: MethodType, argTypes: List[Type], sym: Symbol)(using Context): Type = val ownType = - if !mt.isResultDependent then - mt.resType - else if argTypes.forall(_.isTrackableRef) && isDistinct(argTypes) then - SubstParamsBiMap(mt, argTypes)(mt.resType) - else - SubstParamsMap(mt, argTypes)(mt.resType) + if !mt.isResultDependent then mt.resType + else SubstParamsMap(mt, argTypes)(mt.resType) if sym.isConstructor then refineConstructorInstance(ownType, mt, argTypes, sym) else ownType @@ -722,20 +810,28 @@ class CheckCaptures extends Recheck, SymTransformer: /** First half of result pair: * Refine the type of a constructor call `new C(t_1, ..., t_n)` - * to C{val x_1: T_1, ..., x_m: T_m} where x_1, ..., x_m are the tracked - * parameters of C and T_1, ..., T_m are the types of the corresponding arguments. + * to C{val x_1: @refineOverride T_1, ..., x_m: @refineOverride T_m} + * where x_1, ..., x_m are the tracked parameters of C and + * T_1, ..., T_m are the types of the corresponding arguments. The @refineOveride + * annotations avoid problematic intersections of capture sets when those + * parameters are selected. * * Second half: union of initial capture set and all capture sets of arguments - * to tracked parameters. + * to tracked parameters. The initial capture set `initCs` is augmented with + * - root.Fresh(...) if `core` extends Mutable + * - root.Fresh(...).rd if `core` extends Capability */ def addParamArgRefinements(core: Type, initCs: CaptureSet): (Type, CaptureSet) = var refined: Type = core var allCaptures: CaptureSet = - if core.derivesFromCapability then defn.universalCSImpliedByCapability else initCs + if core.derivesFromMutable then initCs ++ CaptureSet.fresh() + else if core.derivesFromCapability then initCs ++ root.Fresh.withOwner(core.classSymbol).readOnly.singletonCaptureSet + else initCs for (getterName, argType) <- mt.paramNames.lazyZip(argTypes) do val getter = cls.info.member(getterName).suchThat(_.isRefiningParamAccessor).symbol if !getter.is(Private) && getter.hasTrackedParts then - refined = RefinedType(refined, getterName, argType.unboxed) // Yichen you might want to check this + refined = RefinedType(refined, getterName, + AnnotatedType(argType.unboxed, Annotation(defn.RefineOverrideAnnot, util.Spans.NoSpan))) // Yichen you might want to check this allCaptures ++= argType.captureSet (refined, allCaptures) @@ -770,8 +866,8 @@ class CheckCaptures extends Recheck, SymTransformer: case fun @ Select(qual, nme.apply) => qual.symbol.orElse(fun.symbol) case fun => fun.symbol disallowCapInTypeArgs(tree.fun, meth, tree.args) - val res = Existential.toCap(super.recheckTypeApply(tree, pt)) - includeCallCaptures(tree.symbol, res, tree.srcPos) + val res = root.resultToFresh(super.recheckTypeApply(tree, pt)) + includeCallCaptures(tree.symbol, res, tree) checkContains(tree) res end recheckTypeApply @@ -792,7 +888,7 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => override def recheckBlock(tree: Block, pt: Type)(using Context): Type = - inNestedLevel(super.recheckBlock(tree, pt)) + ccState.inNestedLevel(super.recheckBlock(tree, pt)) /** Recheck Closure node: add the captured vars of the anonymoys function * to the result type. See also `recheckClosureBlock` which rechecks the @@ -808,28 +904,53 @@ class CheckCaptures extends Recheck, SymTransformer: * { def $anonfun(...) = ...; closure($anonfun, ...)} */ override def recheckClosureBlock(mdef: DefDef, expr: Closure, pt: Type)(using Context): Type = + + def matchParams(paramss: List[ParamClause], pt: Type): Unit = + //println(i"match $mdef against $pt") + paramss match + case params :: paramss1 => pt match + case defn.PolyFunctionOf(poly: PolyType) => + assert(params.hasSameLengthAs(poly.paramInfos)) + matchParams(paramss1, poly.instantiate(params.map(_.symbol.typeRef))) + case FunctionOrMethod(argTypes, resType) => + assert(params.hasSameLengthAs(argTypes), i"$mdef vs $pt, ${params}") + for (argType, param) <- argTypes.lazyZip(params) do + val paramTpt = param.asInstanceOf[ValDef].tpt + val paramType = root.freshToCap(paramTpt.nuType) + checkConformsExpr(argType, paramType, param) + .showing(i"compared expected closure formal $argType against $param with ${paramTpt.nuType}", capt) + if ccConfig.preTypeClosureResults && !(isEtaExpansion(mdef) && ccConfig.handleEtaExpansionsSpecially) then + // Check whether the closure's result conforms to the expected type + // This constrains parameter types of the closure which can give better + // error messages. + // But if the closure is an eta expanded method reference it's better to not constrain + // its internals early since that would give error messages in generated code + // which are less intelligible. An example is the line `a = x` in + // neg-custom-args/captures/vars.scala. That's why this code is conditioned. + // to apply only to closures that are not eta expansions. + assert(paramss1.isEmpty) + val respt = root.resultToFresh: + pt match + case defn.RefinedFunctionOf(rinfo) => + val paramTypes = params.map(_.asInstanceOf[ValDef].tpt.nuType) + rinfo.instantiate(paramTypes) + case _ => + resType + val res = root.resultToFresh(mdef.tpt.nuType) + // We need to open existentials here in order not to get vars mixed up in them + // We do the proper check with existentials when we are finished with the closure block. + capt.println(i"pre-check closure $expr of type $res against $respt") + checkConformsExpr(res, respt, expr) + case _ => + case Nil => + openClosures = (mdef.symbol, pt) :: openClosures + // openClosures is needed for errors but currently makes no difference + // TODO follow up on this try - // Constrain closure's parameters and result from the expected type before - // rechecking the body. - val res = recheckClosure(expr, pt, forceDependent = true) - if !(isEtaExpansion(mdef) && ccConfig.handleEtaExpansionsSpecially) then - // Check whether the closure's results conforms to the expected type - // This constrains parameter types of the closure which can give better - // error messages. - // But if the closure is an eta expanded method reference it's better to not constrain - // its internals early since that would give error messages in generated code - // which are less intelligible. An example is the line `a = x` in - // neg-custom-args/captures/vars.scala. That's why this code is conditioned. - // to apply only to closures that are not eta expansions. - val res1 = Existential.toCapDeeply(res) - val pt1 = Existential.toCapDeeply(pt) - // We need to open existentials here in order not to get vars mixed up in them - // We do the proper check with existentials when we are finished with the closure block. - capt.println(i"pre-check closure $expr of type $res1 against $pt1") - checkConformsExpr(res1, pt1, expr) + matchParams(mdef.paramss, pt) recheckDef(mdef, mdef.symbol) - res + recheckClosure(expr, pt, forceDependent = true) finally openClosures = openClosures.tail end recheckClosureBlock @@ -872,7 +993,7 @@ class CheckCaptures extends Recheck, SymTransformer: // for more info from the context, so we cannot interpolate. Note that we cannot // expect to have all necessary info available at the point where the anonymous // function is compiled since we do not propagate expected types into blocks. - interpolateVarsIn(tree.tpt) + interpolateIfInferred(tree.tpt, sym) /** Recheck method definitions: * - check body in a nested environment that tracks uses, in a nested level, @@ -900,7 +1021,7 @@ class CheckCaptures extends Recheck, SymTransformer: val saved = curEnv val localSet = capturedVars(sym) - if !localSet.isAlwaysEmpty then + if localSet ne CaptureSet.empty then curEnv = Env(sym, EnvKind.Regular, localSet, curEnv, nestedClosure(tree.rhs)) // ctx with AssumedContains entries for each Contains parameter @@ -912,13 +1033,13 @@ class CheckCaptures extends Recheck, SymTransformer: if ac.isEmpty then ctx else ctx.withProperty(CaptureSet.AssumedContains, Some(ac)) - inNestedLevel: // TODO: nestedLevel needed here? + ccState.inNestedLevel: // TODO: nestedLevel needed here? try checkInferredResult(super.recheckDefDef(tree, sym)(using bodyCtx), tree) finally if !sym.isAnonymousFunction then // Anonymous functions propagate their type to the enclosing environment // so it is not in general sound to interpolate their types. - interpolateVarsIn(tree.tpt) + interpolateIfInferred(tree.tpt, sym) curEnv = saved end recheckDefDef @@ -962,9 +1083,6 @@ class CheckCaptures extends Recheck, SymTransformer: tp end checkInferredResult - /** The set of symbols that were rechecked via a completer */ - private val completed = new mutable.HashSet[Symbol] - /** The normal rechecking if `sym` was already completed before */ override def skipRecheck(sym: Symbol)(using Context): Boolean = completed.contains(sym) @@ -973,7 +1091,7 @@ class CheckCaptures extends Recheck, SymTransformer: * these checks can appear out of order, we need to first create the correct * environment for checking the definition. */ - def completeDef(tree: ValOrDefDef, sym: Symbol)(using Context): Type = + def completeDef(tree: ValOrDefDef, sym: Symbol, completer: LazyType)(using Context): Type = val saved = curEnv try // Setup environment to reflect the new owner. @@ -983,7 +1101,7 @@ class CheckCaptures extends Recheck, SymTransformer: .toMap def restoreEnvFor(sym: Symbol): Env = val localSet = capturedVars(sym) - if localSet.isAlwaysEmpty then rootEnv + if localSet eq CaptureSet.empty then rootEnv else envForOwner.get(sym) match case Some(e) => e case None => Env(sym, EnvKind.Regular, localSet, restoreEnvFor(sym.owner)) @@ -1010,7 +1128,7 @@ class CheckCaptures extends Recheck, SymTransformer: checkSubset(capturedVars(parent.tpe.classSymbol), localSet, parent.srcPos, i"\nof the references allowed to be captured by $cls") val saved = curEnv - if !localSet.isAlwaysEmpty then + if localSet ne CaptureSet.empty then curEnv = Env(cls, EnvKind.Regular, localSet, curEnv) try val thisSet = cls.classInfo.selfType.captureSet.withDescription(i"of the self type of $cls") @@ -1018,7 +1136,8 @@ class CheckCaptures extends Recheck, SymTransformer: for param <- cls.paramGetters do if !param.hasAnnotation(defn.ConstructorOnlyAnnot) && !param.hasAnnotation(defn.UntrackedCapturesAnnot) then - checkSubset(param.termRef.captureSet, thisSet, param.srcPos) // (3) + CCState.withCapAsRoot: // OK? We need this here since self types use `cap` instead of `fresh` + checkSubset(param.termRef.captureSet, thisSet, param.srcPos) // (3) for pureBase <- cls.pureBaseClass do // (4) def selfTypeTree = impl.body .collect: @@ -1034,7 +1153,7 @@ class CheckCaptures extends Recheck, SymTransformer: case AppliedType(fn, args) => disallowCapInTypeArgs(tpt, fn.typeSymbol, args.map(TypeTree(_))) case _ => - inNestedLevelUnless(cls.is(Module)): + ccState.inNestedLevelUnless(cls.is(Module)): super.recheckClassDef(tree, impl, cls) finally curEnv = saved @@ -1048,7 +1167,7 @@ class CheckCaptures extends Recheck, SymTransformer: case AnnotatedType(_, annot) if annot.symbol == defn.RequiresCapabilityAnnot => annot.tree match case Apply(_, cap :: Nil) => - markFree(cap.symbol, tree.srcPos) + markFree(cap.symbol, tree) case _ => case _ => super.recheckTyped(tree) @@ -1058,7 +1177,7 @@ class CheckCaptures extends Recheck, SymTransformer: */ override def recheckTry(tree: Try, pt: Type)(using Context): Type = val tp = super.recheckTry(tree, pt) - if ccConfig.useSealed && Feature.enabled(Feature.saferExceptions) then + if Feature.enabled(Feature.saferExceptions) then disallowRootCapabilitiesIn(tp, ctx.owner, "The result of `try`", "have type", "\nThis is often caused by a locally generated exception capability leaking as part of its result.", @@ -1092,7 +1211,7 @@ class CheckCaptures extends Recheck, SymTransformer: val saved = curEnv tree match case _: RefTree | closureDef(_) if pt.isBoxedCapturing => - curEnv = Env(curEnv.owner, EnvKind.Boxed, CaptureSet.Var(curEnv.owner, level = currentLevel), curEnv) + curEnv = Env(curEnv.owner, EnvKind.Boxed, CaptureSet.Var(curEnv.owner, level = ccState.currentLevel), curEnv) case _ => val res = try @@ -1103,14 +1222,9 @@ class CheckCaptures extends Recheck, SymTransformer: super.recheck(tree, pt) finally curEnv = saved if tree.isTerm && !pt.isBoxedCapturing && pt != LhsProto then - markFree(res.boxedCaptureSet, tree.srcPos) + markFree(res.boxedCaptureSet, tree) res - - /** Under the old unsealed policy: check that cap is ot unboxed */ - override def recheckFinish(tpe: Type, tree: Tree, pt: Type)(using Context): Type = - checkNotUniversalInUnboxedResult(tpe, tree) - super.recheckFinish(tpe, tree, pt) - end recheckFinish + end recheck // ------------------ Adaptation ------------------------------------- // @@ -1127,14 +1241,32 @@ class CheckCaptures extends Recheck, SymTransformer: type BoxErrors = mutable.ListBuffer[Message] | Null - private def boxErrorAddenda(boxErrors: BoxErrors) = - if boxErrors == null then NothingToAdd + private def errorNotes(notes: List[ErrorNote])(using Context): Addenda = + if notes.isEmpty then NothingToAdd else new Addenda: - override def toAdd(using Context): List[String] = - boxErrors.toList.map: msg => - i""" - | - |Note that ${msg.toString}""" + override def toAdd(using Context) = notes.map: note => + val msg = note match + case CompareResult.LevelError(cs, ref) => + if ref.stripReadOnly.isCapOrFresh then + def capStr = if ref.isReadOnly then "cap.rd" else "cap" + i"""the universal capability `$capStr` + |cannot be included in capture set $cs""" + else + val levelStr = ref match + case ref: TermRef => i", defined in ${ref.symbol.maybeOwner}" + case _ => "" + i"""reference ${ref}$levelStr + |cannot be included in outer capture set $cs""" + case ExistentialSubsumesFailure(ex, other) => + def since = + if other.isRootCapability then "" + else " since that capability is not a SharedCapability" + i"""the existential capture root in ${ex.rootAnnot.originalBinder.resType} + |cannot subsume the capability $other$since""" + i""" + | + |Note that ${msg.toString}""" + /** Addendas for error messages that show where we have under-approximated by * mapping a a capture ref in contravariant position to the empty set because @@ -1168,26 +1300,27 @@ class CheckCaptures extends Recheck, SymTransformer: */ override def checkConformsExpr(actual: Type, expected: Type, tree: Tree, addenda: Addenda)(using Context): Type = var expected1 = alignDependentFunction(expected, actual.stripCapturing) - val boxErrors = new mutable.ListBuffer[Message] - val actualBoxed = adapt(actual, expected1, tree.srcPos, boxErrors) + val actualBoxed = adapt(actual, expected1, tree) //println(i"check conforms $actualBoxed <<< $expected1") if actualBoxed eq actual then // Only `addOuterRefs` when there is no box adaptation expected1 = addOuterRefs(expected1, actual, tree.srcPos) - if isCompatible(actualBoxed, expected1) then - if debugSuccesses then tree match - case Ident(_) => - println(i"SUCCESS $tree:\n${TypeComparer.explained(_.isSubType(actual, expected))}") - case _ => - actualBoxed - else - capt.println(i"conforms failed for ${tree}: $actual vs $expected") - err.typeMismatch(tree.withType(actualBoxed), expected1, - addApproxAddenda( - addenda ++ CaptureSet.levelErrors ++ boxErrorAddenda(boxErrors), - expected1)) - actual + ccState.testOK(isCompatible(actualBoxed, expected1)) match + case CompareResult.OK => + if debugSuccesses then tree match + case Ident(_) => + println(i"SUCCESS $tree for $actual <:< $expected:\n${TypeComparer.explained(_.isSubType(actualBoxed, expected1))}") + case _ => + actualBoxed + case fail: CompareFailure => + capt.println(i"conforms failed for ${tree}: $actual vs $expected") + inContext(root.printContext(actualBoxed, expected1)): + err.typeMismatch(tree.withType(actualBoxed), expected1, + addApproxAddenda( + addenda ++ errorNotes(fail.errorNotes), + expected1)) + actual end checkConformsExpr /** Turn `expected` into a dependent function when `actual` is dependent. */ @@ -1203,6 +1336,14 @@ class CheckCaptures extends Recheck, SymTransformer: case defn.RefinedFunctionOf(rinfo: MethodType) => depFun(args, resultType, isContextual, rinfo.paramNames) case _ => expected + case expected @ defn.RefinedFunctionOf(einfo: MethodType) + if einfo.allParamNamesSynthetic => + actual match + case defn.RefinedFunctionOf(ainfo: MethodType) + if !ainfo.allParamNamesSynthetic && ainfo.paramNames.hasSameLengthAs(einfo.paramNames) => + einfo.derivedLambdaType(paramNames = ainfo.paramNames) + .toFunctionType(alwaysDependent = true) + case _ => expected case _ => expected recur(expected) @@ -1232,7 +1373,7 @@ class CheckCaptures extends Recheck, SymTransformer: else if !owner.exists then false else isPure(owner.info) && isPureContext(owner.owner, limit) - // Augment expeced capture set `erefs` by all references in actual capture + // Augment expected capture set `erefs` by all references in actual capture // set `arefs` that are outside some `C.this.type` reference in `erefs` for an enclosing // class `C`. If an added reference is not a ThisType itself, add it to the capture set // (i.e. use set) of the `C`. This makes sure that any outer reference implicitly subsumed @@ -1288,7 +1429,7 @@ class CheckCaptures extends Recheck, SymTransformer: * * @param alwaysConst always make capture set variables constant after adaptation */ - def adaptBoxed(actual: Type, expected: Type, pos: SrcPos, covariant: Boolean, alwaysConst: Boolean, boxErrors: BoxErrors)(using Context): Type = + def adaptBoxed(actual: Type, expected: Type, tree: Tree, covariant: Boolean, alwaysConst: Boolean)(using Context): Type = def recur(actual: Type, expected: Type, covariant: Boolean): Type = @@ -1300,7 +1441,7 @@ class CheckCaptures extends Recheck, SymTransformer: val saved = curEnv curEnv = Env( curEnv.owner, EnvKind.NestedInOwner, - CaptureSet.Var(curEnv.owner, level = currentLevel), + CaptureSet.Var(curEnv.owner, level = ccState.currentLevel), if boxed then null else curEnv) try val (eargs, eres) = expected.dealias.stripCapturing match @@ -1320,21 +1461,11 @@ class CheckCaptures extends Recheck, SymTransformer: def adaptStr = i"adapting $actual ${if covariant then "~~>" else "<~~"} $expected" - // Get existentials and wildcards out of the way - actual match - case actual @ Existential(_, actualUnpacked) => - return Existential.derivedExistentialType(actual): - recur(actualUnpacked, expected, covariant) - case _ => + // Get wildcards out of the way expected match - case expected @ Existential(_, expectedUnpacked) => - return recur(actual, expectedUnpacked, covariant) - case _: WildcardType => - return actual + case _: WildcardType => return actual case _ => - trace(adaptStr, capt, show = true) { - // Decompose the actual type into the inner shape type, the capture set and the box status val actualShape = if actual.isFromJavaObject then actual else actual.stripCapturing val actualIsBoxed = actual.isBoxedCapturing @@ -1352,10 +1483,12 @@ class CheckCaptures extends Recheck, SymTransformer: val cs = actual.captureSet if covariant then cs ++ leaked else - if !leaked.subCaptures(cs, frozen = false).isOK then + if // CCState.withCapAsRoot: // Not sure withCapAsRoot is OK here, actually + !leaked.subCaptures(cs).isOK + then report.error( em"""$expected cannot be box-converted to ${actual.capturing(leaked)} - |since the additional capture set $leaked resulted from box conversion is not allowed in $actual""", pos) + |since the additional capture set $leaked resulting from box conversion is not allowed in $actual""", tree.srcPos) cs def adaptedType(resultBoxed: Boolean) = @@ -1365,34 +1498,12 @@ class CheckCaptures extends Recheck, SymTransformer: .capturing(if alwaysConst then CaptureSet(captures.elems) else captures) .forceBoxStatus(resultBoxed) - if needsAdaptation then - val criticalSet = // the set with which we box or unbox + if needsAdaptation && !insertBox then // we are unboxing + val criticalSet = // the set with which we unbox if covariant then captures // covariant: we box with captures of actual type plus captures leaked by inner adapation else expected.captureSet // contravarant: we unbox with captures of epected type - def msg = em"""$actual cannot be box-converted to $expected - |since at least one of their capture sets contains the root capability `cap`""" - def allowUniversalInBoxed = - ccConfig.useSealed - || expected.hasAnnotation(defn.UncheckedCapturesAnnot) - || actual.widen.hasAnnotation(defn.UncheckedCapturesAnnot) - if !allowUniversalInBoxed then - if criticalSet.isUnboxable && expected.isValueType then - // We can't box/unbox the universal capability. Leave `actual` as it is - // so we get an error in checkConforms. Add the error message generated - // from boxing as an addendum. This tends to give better error - // messages than disallowing the root capability in `criticalSet`. - if boxErrors != null then boxErrors += msg - if ctx.settings.YccDebug.value then - println(i"cannot box/unbox $actual vs $expected") - return actual - // Disallow future addition of `cap` to `criticalSet`. - criticalSet.disallowRootCapability: () => - report.error(msg, pos) - - if !insertBox then // we are unboxing //debugShowEnvs() - markFree(criticalSet, pos) - end if + markFree(criticalSet, tree) // Compute the adapted type. // The result is boxed if actual is boxed and we don't need to adapt, @@ -1403,7 +1514,6 @@ class CheckCaptures extends Recheck, SymTransformer: else adaptedShape .capturing(if alwaysConst then CaptureSet(captures.elems) else captures) .forceBoxStatus(resultIsBoxed) - } end recur recur(actual, expected, covariant) @@ -1418,30 +1528,70 @@ class CheckCaptures extends Recheck, SymTransformer: * Then * foo: Foo { def a: C^{foo}; def b: C^{foo} }^{foo} */ - private def improveCaptures(widened: Type, actual: Type)(using Context): Type = actual match + private def improveCaptures(widened: Type, prefix: Type)(using Context): Type = prefix match case ref: CaptureRef if ref.isTracked => widened match - case CapturingType(p, refs) if ref.singletonCaptureSet.mightSubcapture(refs) => - widened.derivedCapturingType(p, ref.singletonCaptureSet) + case widened @ CapturingType(p, refs) if ref.singletonCaptureSet.mightSubcapture(refs) => + val improvedCs = + if widened.isBoxed then ref.reach.singletonCaptureSet + else ref.singletonCaptureSet + widened.derivedCapturingType(p, improvedCs) .showing(i"improve $widened to $result", capt) case _ => widened case _ => widened + /** If actual is a capturing type T^C extending Mutable, and expected is an + * unboxed non-singleton value type not extending mutable, narrow the capture + * set `C` to `ro(C)`. + * The unboxed condition ensures that the expected type is not a type variable + * that's upper bounded by a read-only type. In this case it would not be sound + * to narrow to the read-only set, since that set can be propagated + * by the type variable instantiation. + */ + private def improveReadOnly(actual: Type, expected: Type)(using Context): Type = actual match + case actual @ CapturingType(parent, refs) + if parent.derivesFrom(defn.Caps_Mutable) + && expected.isValueType + && !expected.isMutableType + && !expected.isSingleton + && !expected.isBoxedCapturing => + actual.derivedCapturingType(parent, refs.readOnly) + case _ => + actual + + /* Currently not needed since it forms part of `adapt` + private def improve(actual: Type, prefix: Type)(using Context): Type = + val widened = actual.widen.dealiasKeepAnnots + val improved = improveCaptures(widened, prefix).withReachCaptures(prefix) + if improved eq widened then actual else improved + */ + + /** An actual singleton type should not be widened if the expected type is a + * LhsProto, or a singleton type, or a path selection with a stable value + */ + private def noWiden(actual: Type, expected: Type)(using Context): Boolean = + actual.isSingleton + && expected.match + case expected: PathSelectionProto => !expected.sym.isOneOf(UnstableValueFlags) + case _ => expected.isSingleton || expected == LhsProto + /** Adapt `actual` type to `expected` type. This involves: * - narrow toplevel captures of `x`'s underlying type to `{x}` according to CC's VAR rule * - narrow nested captures of `x`'s underlying type to `{x*}` * - do box adaptation */ - def adapt(actual: Type, expected: Type, pos: SrcPos, boxErrors: BoxErrors)(using Context): Type = - if expected == LhsProto || expected.isSingleton && actual.isSingleton then + def adapt(actual: Type, expected: Type, tree: Tree)(using Context): Type = + if noWiden(actual, expected) then actual else - val widened = improveCaptures(actual.widen.dealiasKeepAnnots, actual) + val improvedVAR = improveCaptures(actual.widen.dealiasKeepAnnots, actual) + val improved = improveReadOnly(improvedVAR, expected) val adapted = adaptBoxed( - widened.withReachCaptures(actual), expected, pos, - covariant = true, alwaysConst = false, boxErrors) - if adapted eq widened then actual - else adapted.showing(i"adapt boxed $actual vs $expected = $adapted", capt) + improved.withReachCaptures(actual), expected, tree, + covariant = true, alwaysConst = false) + if adapted eq improvedVAR // no .rd improvement, no box-adaptation + then actual // might as well use actual instead of improved widened + else adapted.showing(i"adapt $actual vs $expected = $adapted", capt) end adapt // ---- Unit-level rechecking ------------------------------------------- @@ -1452,50 +1602,82 @@ class CheckCaptures extends Recheck, SymTransformer: * But maybe we can then elide the check during the RefChecks phase under captureChecking? */ def checkOverrides = new TreeTraverser: - class OverridingPairsCheckerCC(clazz: ClassSymbol, self: Type, srcPos: SrcPos)(using Context) extends OverridingPairsChecker(clazz, self): - /** Check subtype with box adaptation. - * This function is passed to RefChecks to check the compatibility of overriding pairs. - * @param sym symbol of the field definition that is being checked - */ - override def checkSubType(actual: Type, expected: Type)(using Context): Boolean = - val expected1 = alignDependentFunction(addOuterRefs(expected, actual, srcPos), actual.stripCapturing) - val actual1 = - val saved = curEnv - try - curEnv = Env(clazz, EnvKind.NestedInOwner, capturedVars(clazz), outer0 = curEnv) - val adapted = - adaptBoxed(actual, expected1, srcPos, covariant = true, alwaysConst = true, null) - actual match - case _: MethodType => - // We remove the capture set resulted from box adaptation for method types, - // since class methods are always treated as pure, and their captured variables - // are charged to the capture set of the class (which is already done during - // box adaptation). - adapted.stripCapturing - case _ => adapted - finally curEnv = saved - actual1 frozen_<:< expected1 + class OverridingPairsCheckerCC(clazz: ClassSymbol, self: Type, tree: Tree)(using Context) extends OverridingPairsChecker(clazz, self): /** Omit the check if one of {overriding,overridden} was nnot capture checked */ override def needsCheck(overriding: Symbol, overridden: Symbol)(using Context): Boolean = !setup.isPreCC(overriding) && !setup.isPreCC(overridden) + /** Perform box adaptation for override checking */ + override def adaptOverridePair(member: Symbol, memberTp: Type, otherTp: Type)(using Context): Option[(Type, Type)] = + if member.isType then + memberTp match + case TypeAlias(_) => + otherTp match + case otherTp: RealTypeBounds => + if otherTp.hi.isBoxedCapturing || otherTp.lo.isBoxedCapturing then + Some((memberTp, otherTp.unboxed)) + else otherTp.hi match + case hi @ CapturingType(parent: TypeRef, refs) + if parent.symbol == defn.Caps_CapSet && refs.isUniversal => + Some(( + memberTp, + otherTp.derivedTypeBounds( + otherTp.lo, + hi.derivedCapturingType(parent, root.Fresh().singletonCaptureSet)))) + case _ => None + case _ => None + case _ => None + else memberTp match + case memberTp @ ExprType(memberRes) => + adaptOverridePair(member, memberRes, otherTp) match + case Some((mres, otp)) => Some((memberTp.derivedExprType(mres), otp)) + case None => None + case _ => otherTp match + case otherTp @ ExprType(otherRes) => + adaptOverridePair(member, memberTp, otherRes) match + case Some((mtp, ores)) => Some((mtp, otherTp.derivedExprType(ores))) + case None => None + case _ => + val expected1 = alignDependentFunction(addOuterRefs(otherTp, memberTp, tree.srcPos), memberTp.stripCapturing) + val actual1 = + val saved = curEnv + try + curEnv = Env(clazz, EnvKind.NestedInOwner, capturedVars(clazz), outer0 = curEnv) + val adapted = + adaptBoxed(memberTp, expected1, tree, covariant = true, alwaysConst = true) + memberTp match + case _: MethodType => + // We remove the capture set resulted from box adaptation for method types, + // since class methods are always treated as pure, and their captured variables + // are charged to the capture set of the class (which is already done during + // box adaptation). + adapted.stripCapturing + case _ => adapted + finally curEnv = saved + if (actual1 eq memberTp) && (expected1 eq otherTp) then None + else Some((actual1, expected1)) + end adaptOverridePair + override def checkInheritedTraitParameters: Boolean = false - /** Check that overrides don't change the @use status of their parameters */ + /** Check that overrides don't change the @use or @consume status of their parameters */ override def additionalChecks(member: Symbol, other: Symbol)(using Context): Unit = for (params1, params2) <- member.rawParamss.lazyZip(other.rawParamss) (param1, param2) <- params1.lazyZip(params2) do - if param1.hasAnnotation(defn.UseAnnot) != param2.hasAnnotation(defn.UseAnnot) then - report.error( - OverrideError( - i"has a parameter ${param1.name} with different @use status than the corresponding parameter in the overridden definition", - self, member, other, self.memberInfo(member), self.memberInfo(other) - ), - if member.owner == clazz then member.srcPos else clazz.srcPos - ) + def checkAnnot(cls: ClassSymbol) = + if param1.hasAnnotation(cls) != param2.hasAnnotation(cls) then + report.error( + OverrideError( + i"has a parameter ${param1.name} with different @${cls.name} status than the corresponding parameter in the overridden definition", + self, member, other, self.memberInfo(member), self.memberInfo(other) + ), + if member.owner == clazz then member.srcPos else clazz.srcPos) + + checkAnnot(defn.UseAnnot) + checkAnnot(defn.ConsumeAnnot) end OverridingPairsCheckerCC def traverse(t: Tree)(using Context) = @@ -1526,7 +1708,7 @@ class CheckCaptures extends Recheck, SymTransformer: def traverse(tree: Tree)(using Context) = tree match case id: Ident => val sym = id.symbol - if sym.is(Mutable, butNot = Method) && sym.owner.isTerm then + if sym.isMutableVar && sym.owner.isTerm then val enclMeth = ctx.owner.enclosingMethod if sym.enclosingMethod != enclMeth then capturedBy(sym) = enclMeth @@ -1554,7 +1736,19 @@ class CheckCaptures extends Recheck, SymTransformer: report.echo(s"$echoHeader\n$treeString\n") withCaptureSetsExplained: - super.checkUnit(unit) + def iterate(): Unit = + super.checkUnit(unit) + if !ctx.reporter.errorsReported + && (needAnotherRun + || ccConfig.alwaysRepeatRun && ccState.iterationId == 1) + then + resetIteration() + ccState.nextIteration: + setup.setupUnit(unit.tpdTree, this) + capt.println(s"**** capture checking run ${ccState.iterationId} started on ${ctx.source}") + iterate() + + iterate() checkOverrides.traverse(unit.tpdTree) postCheck(unit.tpdTree) checkSelfTypes(unit.tpdTree) @@ -1597,11 +1791,11 @@ class CheckCaptures extends Recheck, SymTransformer: inContext(ctx.fresh.setOwner(root)): checkSelfAgainstParents(root, root.baseClasses) val selfType = root.asClass.classInfo.selfType - interpolator(startingVariance = -1).traverse(selfType) + interpolate(selfType, root, startingVariance = -1) selfType match case CapturingType(_, refs: CaptureSet.Var) if !root.isEffectivelySealed - && !refs.elems.exists(_.isRootCapability) + && !refs.isUniversal && !root.matchesExplicitRefsInBaseClass(refs) => // Forbid inferred self types unless they are already implied by an explicit @@ -1616,65 +1810,26 @@ class CheckCaptures extends Recheck, SymTransformer: capt.println(i"checked $root with $selfType") end checkSelfTypes - /** Heal ill-formed capture sets in the type parameter. - * - * We can push parameter refs into a capture set in type parameters - * that this type parameter can't see. - * For example, when capture checking the following expression: - * - * def usingLogFile[T](op: File^ => T): T = ... - * - * usingLogFile[box ?1 () -> Unit] { (f: File^) => () => { f.write(0) } } - * - * We may propagate `f` into ?1, making ?1 ill-formed. - * This also causes soundness issues, since `f` in ?1 should be widened to `cap`, - * giving rise to an error that `cap` cannot be included in a boxed capture set. - * - * To solve this, we still allow ?1 to capture parameter refs like `f`, but - * compensate this by pushing the widened capture set of `f` into ?1. - * This solves the soundness issue caused by the ill-formness of ?1. + /** Check ill-formed capture sets in a type parameter. We used to be able to + * push parameter refs into a capture set in type parameters that this type + * parameter can't see. We used to heal this by replacing illegal refs by their + * underlying capture sets. But now these should no longer be necessary, so + * instead of errors we use assertions. */ - private def healTypeParam(tree: Tree, paramName: TypeName, meth: Symbol)(using Context): Unit = + private def checkTypeParam(tree: Tree, paramName: TypeName, meth: Symbol)(using Context): Unit = val checker = new TypeTraverser: private var allowed: SimpleIdentitySet[TermParamRef] = SimpleIdentitySet.empty - private def isAllowed(ref: CaptureRef): Boolean = ref match - case ref: TermParamRef => allowed.contains(ref) - case _ => true - - private def healCaptureSet(cs: CaptureSet): Unit = - cs.ensureWellformed: elem => - ctx ?=> - var seen = new util.HashSet[CaptureRef] - def recur(ref: CaptureRef): Unit = ref.stripReach match - case ref: TermParamRef - if !allowed.contains(ref) && !seen.contains(ref) => - seen += ref - if ref.isMaxCapability then - report.error(i"escaping local reference $ref", tree.srcPos) - else - val widened = ref.captureSetOfInfo - 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.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 $paramInfo leaks into outer capture set$debugSetInfo of type parameter $paramName$location", - tree.srcPos) - else - widened.elems.foreach(recur) - case _ => - recur(elem) + private def checkCaptureSet(cs: CaptureSet): Unit = + for elem <- cs.elems do + elem.stripReach match + case ref: TermParamRef => assert(allowed.contains(ref)) + case _ => def traverse(tp: Type) = tp match case CapturingType(parent, refs) => - healCaptureSet(refs) + checkCaptureSet(refs) traverse(parent) case defn.RefinedFunctionOf(rinfo: MethodType) => traverse(rinfo) @@ -1689,7 +1844,7 @@ class CheckCaptures extends Recheck, SymTransformer: if tree.isInstanceOf[InferredTypeTree] then checker.traverse(tree.nuType) - end healTypeParam + end checkTypeParam /** Under the unsealed policy: Arrays are like vars, check that their element types * do not contains `cap` (in fact it would work also to check on array creation @@ -1713,9 +1868,7 @@ class CheckCaptures extends Recheck, SymTransformer: traverseChildren(t) check.traverse(tp) - /** Perform the following kinds of checks - * - Check that arguments of TypeApplys and AppliedTypes conform to their bounds. - * - Heal ill-formed capture sets of type parameters. See `healTypeParam`. + /** Check that arguments of TypeApplys and AppliedTypes conform to their bounds. */ def postCheck(unit: tpd.Tree)(using Context): Unit = val checker = new TreeTraverser: @@ -1733,16 +1886,17 @@ class CheckCaptures extends Recheck, SymTransformer: val normArgs = args.lazyZip(tl.paramInfos).map: (arg, bounds) => arg.withType(arg.nuType.forceBoxStatus( bounds.hi.isBoxedCapturing | bounds.lo.isBoxedCapturing)) - checkBounds(normArgs, tl) - args.lazyZip(tl.paramNames).foreach(healTypeParam(_, _, fun.symbol)) + CCState.withCapAsRoot: // OK? We need this since bounds use `cap` instead of `fresh` + checkBounds(normArgs, tl) + if ccConfig.postCheckCapturesets then + args.lazyZip(tl.paramNames).foreach(checkTypeParam(_, _, fun.symbol)) case _ => - case tree: TypeTree if !ccConfig.useSealed => - checkArraysAreSealedIn(tree.tpe, tree.srcPos) case _ => end check end checker checker.traverse(unit)(using ctx.withOwner(defn.RootClass)) + if ccConfig.useSepChecks then SepCheck(this).traverse(unit) if !ctx.reporter.errorsReported then // We dont report errors here if previous errors were reported, because other // errors often result in bad applied types, but flagging these bad types gives @@ -1751,7 +1905,9 @@ class CheckCaptures extends Recheck, SymTransformer: def traverse(t: Tree)(using Context) = t match case tree: InferredTypeTree => case tree: New => - case tree: TypeTree => checkAppliedTypesIn(tree.withType(tree.nuType)) + case tree: TypeTree => + CCState.withCapAsRoot: + checkAppliedTypesIn(tree.withType(tree.nuType)) case _ => traverseChildren(t) checkApplied.traverse(unit) end postCheck diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala deleted file mode 100644 index ea979e0b9f7f..000000000000 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ /dev/null @@ -1,385 +0,0 @@ -package dotty.tools -package dotc -package cc - -import core.* -import Types.*, Symbols.*, Contexts.*, Annotations.*, Flags.* -import CaptureSet.IdempotentCaptRefMap -import StdNames.nme -import ast.tpd.* -import Decorators.* -import typer.ErrorReporting.errorType -import Names.TermName -import NameKinds.ExistentialBinderName -import NameOps.isImpureFunction -import reporting.Message - -/** - -Handling existentials in CC: - - - We generally use existentials only in function and method result types - - All occurrences of an EX-bound variable appear co-variantly in the bound type - -In Setup: - - - Convert occurrences of `cap` in function results to existentials. Precise rules below. - - Conversions are done in two places: - - + As part of mapping from local types of parameters and results to infos of methods. - The local types just use `cap`, whereas the result type in the info uses EX-bound variables. - + When converting functions or methods appearing in explicitly declared types. - Here again, we only replace cap's in fucntion results. - - - Conversion is done with a BiTypeMap in `Existential.mapCap`. - -In reckeckApply and recheckTypeApply: - - - If an EX is toplevel in the result type, replace its bound variable - occurrences with `cap`. - -Level checking and avoidance: - - - Environments, capture refs, and capture set variables carry levels - - + levels start at 0 - + The level of a block or template statement sequence is one higher than the level of - its environment - + The level of a TermRef is the level of the environment where its symbol is defined. - + The level of a ThisType is the level of the statements of the class to which it beloongs. - + The level of a TermParamRef is currently -1 (i.e. TermParamRefs are not yet checked using this system) - + The level of a capture set variable is the level of the environment where it is created. - - - Variables also carry info whether they accept `cap` or not. Variables introduced under a box - don't, the others do. - - - Capture set variables do not accept elements of level higher than the variable's level - - - We use avoidance to heal such cases: If the level-incorrect ref appears - + covariantly: widen to underlying capture set, reject if that is cap and the variable does not allow it - + contravariantly: narrow to {} - + invarianty: reject with error - -In cv-computation (markFree): - - - Reach capabilities x* of a parameter x cannot appear in the capture set of - the owning method. They have to be widened to dcs(x), or, where this is not - possible, it's an error. - -In box adaptation: - - - Check that existential variables are not boxed or unboxed. - -Subtype rules - - - new alphabet: existentially bound variables `a`. - - they can be stored in environments Gamma. - - they are alpha-renable, usual hygiene conditions apply - - Gamma |- EX a.T <: U - if Gamma, a |- T <: U - - Gamma |- T <: EX a.U - if exists capture set C consisting of capture refs and ex-bound variables - bound in Gamma such that Gamma |- T <: [a := C]U - -Representation: - - EX a.T[a] is represented as a dependent function type - - (a: Exists) => T[a]] - - where Exists is defined in caps like this: - - sealed trait Exists extends Capability - - The defn.RefinedFunctionOf extractor will exclude existential types from - its results, so only normal refined functions match. - - Let `boundvar(ex)` be the TermParamRef defined by the existential type `ex`. - -Subtype checking algorithm, general scheme: - - Maintain two structures in TypeComparer: - - openExistentials: List[TermParamRef] - assocExistentials: Map[TermParamRef, List[TermParamRef]] - - `openExistentials` corresponds to the list of existential variables stored in the environment. - `assocExistentials` maps existential variables bound by existentials appearing on the right - to the value of `openExistentials` at the time when the existential on the right was dropped. - -Subtype checking algorithm, steps to add for tp1 <:< tp2: - - If tp1 is an existential EX a.tp1a: - - val saved = openExistentials - openExistentials = boundvar(tp1) :: openExistentials - try tp1a <:< tp2 - finally openExistentials = saved - - If tp2 is an existential EX a.tp2a: - - val saved = assocExistentials - assocExistentials = assocExistentials + (boundvar(tp2) -> openExistentials) - try tp1 <:< tp2a - finally assocExistentials = saved - - If tp2 is an existentially bound variable: - assocExistentials(tp2).isDefined - && (assocExistentials(tp2).contains(tp1) || tp1 is not existentially bound) - -Subtype checking algorithm, comparing two capture sets CS1 <:< CS2: - - We need to map the (possibly to-be-added) existentials in CS1 to existentials - in CS2 so that we can compare them. We use `assocExistentals` for that: - To map an EX-variable V1 in CS1, pick the last (i.e. outermost, leading to the smallest - type) EX-variable in `assocExistentials` that has V1 in its possible instances. - To go the other way (and therby produce a BiTypeMap), map an EX-variable - V2 in CS2 to the first (i.e. innermost) EX-variable it can be instantiated to. - If either direction is not defined, we choose a special "bad-existetal" value - that represents and out-of-scope existential. This leads to failure - of the comparison. - -Existential source syntax: - - Existential types are ususally not written in source, since we still allow the `^` - syntax that can express most of them more concesely (see below for translation rules). - But we should also allow to write existential types explicity, even if it ends up mainly - for debugging. To express them, we use the encoding with `Exists`, so a typical - expression of an existential would be - - (x: Exists) => A ->{x} B - - Existential types can only at the top level of the result type - of a function or method. - -Restrictions on Existential Types: (to be implemented if we want to -keep the source syntax for users). - - - An existential capture ref must be the only member of its set. This is - intended to model the idea that existential variables effectibely range - over capture sets, not capture references. But so far our calculus - and implementation does not yet acoommodate first-class capture sets. - - Existential capture refs must appear co-variantly in their bound type - - So the following would all be illegal: - - EX x.C^{x, io} // error: multiple members - EX x.() => EX y.C^{x, y} // error: multiple members - EX x.C^{x} ->{x} D // error: contra-variant occurrence - EX x.Set[C^{x}] // error: invariant occurrence - -Expansion of ^: - - We expand all occurrences of `cap` in the result types of functions or methods - to existentially quantified types. Nested scopes are expanded before outer ones. - - The expansion algorithm is then defined as follows: - - 1. In a result type, replace every occurrence of ^ with a fresh existentially - bound variable and quantify over all variables such introduced. - - 2. After this step, type aliases are expanded. If aliases have aliases in arguments, - the outer alias is expanded before the aliases in the arguments. Each time an alias - is expanded that reveals a `^`, apply step (1). - - 3. The algorithm ends when no more alieases remain to be expanded. - - Examples: - - - `A => B` is an alias type that expands to `(A -> B)^`, therefore - `() -> A => B` expands to `() -> EX c. A ->{c} B`. - - - `() => Iterator[A => B]` expands to `() => EX c. Iterator[A ->{c} B]` - - - `A -> B^` expands to `A -> EX c.B^{c}`. - - - If we define `type Fun[T] = A -> T`, then `() -> Fun[B^]` expands to `() -> EX c.Fun[B^{c}]`, which - dealiases to `() -> EX c.A -> B^{c}`. - - - If we define - - type F = A -> Fun[B^] - - then the type alias expands to - - type F = A -> EX c.A -> B^{c} -*/ -object Existential: - - type Carrier = RefinedType - - def unapply(tp: Carrier)(using Context): Option[(TermParamRef, Type)] = - tp.refinedInfo match - case mt: MethodType - if isExistentialMethod(mt) && defn.isNonRefinedFunction(tp.parent) => - Some(mt.paramRefs.head, mt.resultType) - case _ => None - - /** Create method type in the refinement of an existential type */ - private def exMethodType(using Context)( - mk: TermParamRef => Type, - boundName: TermName = ExistentialBinderName.fresh() - ): MethodType = - MethodType(boundName :: Nil)( - mt => defn.Caps_Exists.typeRef :: Nil, - mt => mk(mt.paramRefs.head)) - - /** Create existential */ - def apply(mk: TermParamRef => Type)(using Context): Type = - exMethodType(mk).toFunctionType(alwaysDependent = true) - - /** Create existential if bound variable appears in result of `mk` */ - def wrap(mk: TermParamRef => Type)(using Context): Type = - val mt = exMethodType(mk) - if mt.isResultDependent then mt.toFunctionType() else mt.resType - - extension (tp: Carrier) - def derivedExistentialType(core: Type)(using Context): Type = tp match - case Existential(boundVar, unpacked) => - if core eq unpacked then tp - else apply(bv => core.substParam(boundVar, bv)) - case _ => - core - - /** Map top-level existentials to `cap`. Do the same for existentials - * in function results if all preceding arguments are known to be always pure. - */ - def toCap(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match - case Existential(boundVar, unpacked) => - val transformed = unpacked.substParam(boundVar, defn.captureRoot.termRef) - transformed match - case FunctionOrMethod(args, res @ Existential(_, _)) - if args.forall(_.isAlwaysPure) => - transformed.derivedFunctionOrMethod(args, toCap(res)) - case _ => - transformed - case tp1 @ CapturingType(parent, refs) => - tp1.derivedCapturingType(toCap(parent), refs) - case tp1 @ AnnotatedType(parent, ann) => - tp1.derivedAnnotatedType(toCap(parent), ann) - case _ => tp - - /** Map existentials at the top-level and in all nested result types to `cap` - */ - def toCapDeeply(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match - case Existential(boundVar, unpacked) => - toCapDeeply(unpacked.substParam(boundVar, defn.captureRoot.termRef)) - case tp1 @ FunctionOrMethod(args, res) => - val tp2 = tp1.derivedFunctionOrMethod(args, toCapDeeply(res)) - if tp2 ne tp1 then tp2 else tp - case tp1 @ CapturingType(parent, refs) => - tp1.derivedCapturingType(toCapDeeply(parent), refs) - case tp1 @ AnnotatedType(parent, ann) => - tp1.derivedAnnotatedType(toCapDeeply(parent), ann) - case _ => tp - - /** Knowing that `tp` is a function type, is an alias to a function other - * than `=>`? - */ - private def isAliasFun(tp: Type)(using Context) = tp match - case AppliedType(tycon, _) => !defn.isFunctionSymbol(tycon.typeSymbol) - case _ => false - - /** Replace all occurrences of `cap` in parts of this type by an existentially bound - * variable. If there are such occurrences, or there might be in the future due to embedded - * capture set variables, create an existential with the variable wrapping the type. - * Stop at function or method types since these have been mapped before. - */ - def mapCap(tp: Type, fail: Message => Unit)(using Context): Type = - var needsWrap = false - - abstract class CapMap extends BiTypeMap: - override def mapOver(t: Type): Type = t match - case t @ FunctionOrMethod(args, res) if variance > 0 && !isAliasFun(t) => - t // `t` should be mapped in this case by a different call to `mapCap`. - case Existential(_, _) => - t - case t: (LazyRef | TypeVar) => - mapConserveSuper(t) - case _ => - super.mapOver(t) - - class Wrap(boundVar: TermParamRef) extends CapMap: - def apply(t: Type) = t match - case t: TermRef if t.isRootCapability => - if variance > 0 then - needsWrap = true - boundVar - else - if variance == 0 then - fail(em"""$tp captures the root capability `cap` in invariant position""") - // we accept variance < 0, and leave the cap as it is - super.mapOver(t) - case t @ CapturingType(parent, refs: CaptureSet.Var) => - if variance > 0 then needsWrap = true - super.mapOver(t) - case defn.FunctionNOf(args, res, contextual) if t.typeSymbol.name.isImpureFunction => - if variance > 0 then - needsWrap = true - super.mapOver: - defn.FunctionNOf(args, res, contextual).capturing(boundVar.singletonCaptureSet) - else mapOver(t) - case _ => - mapOver(t) - //.showing(i"mapcap $t = $result") - - lazy val inverse = new BiTypeMap: - def apply(t: Type) = t match - case t: TermParamRef if t eq boundVar => defn.captureRoot.termRef - case _ => mapOver(t) - def inverse = Wrap.this - override def toString = "Wrap.inverse" - end Wrap - - val wrapped = apply(Wrap(_)(tp)) - if needsWrap then wrapped else tp - end mapCap - - /** Map `cap` in function results to fresh existentials */ - def mapCapInResults(fail: Message => Unit)(using Context): TypeMap = new: - - def mapFunOrMethod(tp: Type, args: List[Type], res: Type): Type = - val args1 = atVariance(-variance)(args.map(this)) - val res1 = res match - case res: MethodType => mapFunOrMethod(res, res.paramInfos, res.resType) - case res: PolyType => mapFunOrMethod(res, Nil, res.resType) // TODO: Also map bounds of PolyTypes - case _ => mapCap(apply(res), fail) - //.showing(i"map cap res $res / ${apply(res)} of $tp = $result") - tp.derivedFunctionOrMethod(args1, res1) - - def apply(t: Type): Type = t match - case FunctionOrMethod(args, res) if variance > 0 && !isAliasFun(t) => - mapFunOrMethod(t, args, res) - case CapturingType(parent, refs) => - t.derivedCapturingType(this(parent), refs) - case Existential(_, _) => - t - case t: (LazyRef | TypeVar) => - mapConserveSuper(t) - case _ => - mapOver(t) - end mapCapInResults - - /** Is `mt` a method represnting an existential type when used in a refinement? */ - def isExistentialMethod(mt: TermLambda)(using Context): Boolean = mt.paramInfos match - case (info: TypeRef) :: rest => info.symbol == defn.Caps_Exists && rest.isEmpty - case _ => false - - /** Is `ref` this an existentially bound variable? */ - def isExistentialVar(ref: CaptureRef)(using Context) = ref match - case ref: TermParamRef => isExistentialMethod(ref.binder) - case _ => false - - /** An value signalling an out-of-scope existential that should - * lead to a compare failure. - */ - def badExistential(using Context): TermParamRef = - exMethodType(identity, nme.OOS_EXISTENTIAL).paramRefs.head - - def isBadExistential(ref: CaptureRef) = ref match - case ref: TermParamRef => ref.paramName == nme.OOS_EXISTENTIAL - case _ => false - -end Existential diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala new file mode 100644 index 000000000000..6434db0638a3 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -0,0 +1,960 @@ +package dotty.tools +package dotc +package cc +import ast.tpd +import collection.mutable + +import core.* +import Symbols.*, Types.*, Flags.* +import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.* +import CaptureSet.{Refs, emptyRefs, HiddenSet} +import config.Printers.capt +import StdNames.nme +import util.{SimpleIdentitySet, EqHashMap, SrcPos} +import tpd.* +import reflect.ClassTag +import reporting.trace + +/** The separation checker is a tree traverser that is run after capture checking. + * It checks tree nodes for various separation conditions, explained in the + * methods below. Rough summary: + * + * - Hidden sets of arguments must not be referred to in the same application + * - Hidden sets of (result-) types must not be referred to alter in the same scope. + * - Returned hidden sets can only refer to @consume parameters. + * - If returned hidden sets refer to an encloding this, the reference must be + * from a @consume method. + * - Consumed entities cannot be used subsequently. + * - Entitites cannot be consumed in a loop. + */ +object SepCheck: + + /** Enumerates kinds of captures encountered so far */ + enum Captures: + case None + case Explicit // one or more explicitly declared captures + case Hidden // exacttly one hidden captures + case NeedsCheck // one hidden capture and one other capture (hidden or declared) + + def add(that: Captures): Captures = + if this == None then that + else if that == None then this + else if this == Explicit && that == Explicit then Explicit + else NeedsCheck + end Captures + + /** The role in which a checked type appears, used for composing error messages */ + enum TypeRole: + case Result(sym: Symbol, inferred: Boolean) + case Argument(arg: Tree) + case Qualifier(qual: Tree, meth: Symbol) + + /** If this is a Result tole, the associated symbol, otherwise NoSymbol */ + def dclSym = this match + case Result(sym, _) => sym + case _ => NoSymbol + + /** A textual description of this role */ + def description(using Context): String = this match + case Result(sym, inferred) => + def inferredStr = if inferred then " inferred" else "" + def resultStr = if sym.info.isInstanceOf[MethodicType] then " result" else "" + i"$sym's$inferredStr$resultStr type" + case TypeRole.Argument(_) => + "the argument's adapted type" + case TypeRole.Qualifier(_, meth) => + i"the type of the prefix to a call of $meth" + end TypeRole + + /** A class for segmented sets of consumed references. + * References are associated with the source positions where they first appeared. + * References are compared with `eq`. + */ + abstract class ConsumedSet: + /** The references in the set. The array should be treated as immutable in client code */ + def refs: Array[CaptureRef] + + /** The associated source positoons. The array should be treated as immutable in client code */ + def locs: Array[SrcPos] + + /** The number of references in the set */ + def size: Int + + def toMap: Map[CaptureRef, SrcPos] = refs.take(size).zip(locs).toMap + + def show(using Context) = + s"[${toMap.map((ref, loc) => i"$ref -> $loc").toList}]" + end ConsumedSet + + /** A fixed consumed set consisting of the given references `refs` and + * associated source positions `locs` + */ + class ConstConsumedSet(val refs: Array[CaptureRef], val locs: Array[SrcPos]) extends ConsumedSet: + def size = refs.size + + /** A mutable consumed set, which is initially empty */ + class MutConsumedSet extends ConsumedSet: + var refs: Array[CaptureRef] = new Array(4) + var locs: Array[SrcPos] = new Array(4) + var size = 0 + var peaks: Refs = emptyRefs + + private def double[T <: AnyRef : ClassTag](xs: Array[T]): Array[T] = + val xs1 = new Array[T](xs.length * 2) + xs.copyToArray(xs1) + xs1 + + private def ensureCapacity(added: Int): Unit = + if size + added > refs.length then + refs = double(refs) + locs = double(locs) + + /** If `ref` is in the set, its associated source position, otherwise `null` */ + def get(ref: CaptureRef): SrcPos | Null = + var i = 0 + while i < size && (refs(i) ne ref) do i += 1 + if i < size then locs(i) else null + + def clashing(ref: CaptureRef)(using Context): SrcPos | Null = + val refPeaks = ref.peaks + if !peaks.sharedWith(refPeaks).isEmpty then + var i = 0 + while i < size && refs(i).peaks.sharedWith(refPeaks).isEmpty do + i += 1 + assert(i < size) + locs(i) + else null + + /** If `ref` is not yet in the set, add it with given source position */ + def put(ref: CaptureRef, loc: SrcPos)(using Context): Unit = + if get(ref) == null then + ensureCapacity(1) + refs(size) = ref + locs(size) = loc + size += 1 + peaks = peaks ++ ref.peaks + + /** Add all references with their associated positions from `that` which + * are not yet in the set. + */ + def ++= (that: ConsumedSet)(using Context): Unit = + for i <- 0 until that.size do put(that.refs(i), that.locs(i)) + + /** Run `op` and return any new references it created in a separate `ConsumedSet`. + * The current mutable set is reset to its state before `op` was run. + */ + def segment(op: => Unit): ConsumedSet = + val start = size + val savedPeaks = peaks + try + op + if size == start then EmptyConsumedSet + else ConstConsumedSet(refs.slice(start, size), locs.slice(start, size)) + finally + size = start + peaks = savedPeaks + end MutConsumedSet + + val EmptyConsumedSet = ConstConsumedSet(Array(), Array()) + + case class PeaksPair(actual: Refs, hidden: Refs) + + case class DefInfo(tree: ValOrDefDef, symbol: Symbol, hidden: Refs, hiddenPeaks: Refs) + + extension (refs: Refs) + + /** The footprint of a set of references `refs` the smallest set `F` such that + * 1. if includeMax is false then no maximal capability is in `F` + * 2. all capabilities in `refs` satisfying (1) are in `F` + * 3. if `f in F` then the footprint of `f`'s info is also in `F`. + */ + private def footprint(includeMax: Boolean = false)(using Context): Refs = + def retain(ref: CaptureRef) = includeMax || !ref.isRootCapability + def recur(elems: Refs, newElems: List[CaptureRef]): Refs = newElems match + case newElem :: newElems1 => + val superElems = newElem.captureSetOfInfo.elems.filter: superElem => + retain(superElem) && !elems.contains(superElem) + recur(elems ++ superElems, newElems1 ++ superElems.toList) + case Nil => elems + val elems: Refs = refs.filter(retain) + recur(elems, elems.toList) + + private def peaks(using Context): Refs = + def recur(seen: Refs, acc: Refs, newElems: List[CaptureRef]): Refs = trace(i"peaks $acc, $newElems = "): + newElems match + case newElem :: newElems1 => + if seen.contains(newElem) then + recur(seen, acc, newElems1) + else newElem.stripReadOnly match + case root.Fresh(hidden) => + if hidden.deps.isEmpty then recur(seen + newElem, acc + newElem, newElems1) + else + val superCaps = + if newElem.isReadOnly then hidden.superCaps.map(_.readOnly) + else hidden.superCaps + recur(seen + newElem, acc, superCaps ++ newElems) + case _ => + if newElem.isRootCapability + //|| newElem.isInstanceOf[TypeRef | TypeParamRef] + then recur(seen + newElem, acc, newElems1) + else recur(seen + newElem, acc, newElem.captureSetOfInfo.elems.toList ++ newElems1) + case Nil => acc + recur(emptyRefs, emptyRefs, refs.toList) + + /** The shared peaks between `refs` and `other` */ + private def sharedWith(other: Refs)(using Context): Refs = + def common(refs1: Refs, refs2: Refs) = + refs1.filter: ref => + !ref.isReadOnly && refs2.exists(_.stripReadOnly eq ref) + common(refs, other) ++ common(other, refs) + + /** The overlap of two footprint sets F1 and F2. This contains all exclusive references `r` + * such that one of the following is true: + * 1. + * - one of the sets contains `r` + * - the other contains a capability `s` or `s.rd` where `s` _covers_ `r` + * 2. + * - one of the sets contains `r.rd` + * - the other contains a capability `s` where `s` _covers_ `r` + * + * A capability `s` covers `r` if `r` can be seen as a path extension of `s`. E.g. + * if `s = x.a` and `r = x.a.b.c` then `s` covers `a`. + */ + private def overlapWith(other: Refs)(using Context): Refs = + val refs1 = refs + val refs2 = other + + /** Exclusive capabilities in refs1 that are covered by exclusive or + * stripped read-only capabilties in refs2 + * + stripped read-only capabilities in refs1 that are covered by an + * exclusive capability in refs2. + */ + def common(refs1: Refs, refs2: Refs) = + refs1.filter: ref => + ref.isExclusive && refs2.exists(_.stripReadOnly.covers(ref)) + ++ + refs1 + .filter: + case ReadOnlyCapability(ref @ TermRef(prefix: CaptureRef, _)) => + // We can get away testing only references with at least one field selection + // here since stripped readOnly references that equal a reference in refs2 + // are added by the first clause of the symmetric call to common. + !ref.isCap && refs2.exists(_.covers(prefix)) + case _ => + false + .map(_.stripReadOnly) + + common(refs, other) ++ common(other, refs) + end overlapWith + + /** The non-maximal elements hidden directly or indirectly by a maximal + * capability in `refs`. E g. if `R = {x, >}` then + * its hidden set is `{y, z}`. + */ + private def hiddenSet(using Context): Refs = + val seen: util.EqHashSet[CaptureRef] = new util.EqHashSet + + def hiddenByElem(elem: CaptureRef): Refs = elem match + case root.Fresh(hcs) => hcs.elems ++ recur(hcs.elems) + case ReadOnlyCapability(ref1) => hiddenByElem(ref1).map(_.readOnly) + case _ => emptyRefs + + def recur(refs: Refs): Refs = + (emptyRefs /: refs): (elems, elem) => + if seen.add(elem) then elems ++ hiddenByElem(elem) else elems + + recur(refs) + end hiddenSet + + /** Subtract all elements that are covered by some element in `others` from this set. */ + private def deduct(others: Refs)(using Context): Refs = + refs.filter: ref => + !others.exists(_.covers(ref)) + + /** Deduct `sym` and `sym*` from `refs` */ + private def deductSymRefs(sym: Symbol)(using Context): Refs = + val ref = sym.termRef + if ref.isTrackableRef then refs.deduct(SimpleIdentitySet(ref, ref.reach)) + else refs + + end extension + + extension (ref: CaptureRef) + def peaks(using Context): Refs = SimpleIdentitySet(ref).peaks + +class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: + import checker.* + import SepCheck.* + + /** The set of capabilities that are hidden by a polymorphic result type + * of some previous definition. + */ + private var defsShadow: Refs = emptyRefs + + /** The previous val or def definitions encountered during separation checking + * in reverse order. These all enclose and precede the current traversal node. + */ + private var previousDefs: List[DefInfo] = Nil + + /** The set of references that were consumed so far in the current method */ + private var consumed: MutConsumedSet = MutConsumedSet() + + /** Infos aboput Labeled expressions enclosing the current traversal point. + * For each labeled expression, it's label name, and a list buffer containing + * all consumed sets of return expressions referring to that label. + */ + private var openLabeled: List[(Name, mutable.ListBuffer[ConsumedSet])] = Nil + + /** The deep capture set of an argument or prefix widened to the formal parameter, if + * the latter contains a cap. + */ + private def formalCaptures(arg: Tree)(using Context): Refs = + arg.formalType.orElse(arg.nuType).deepCaptureSet.elems + + /** The deep capture set if the type of `tree` */ + private def captures(tree: Tree)(using Context): Refs = + tree.nuType.deepCaptureSet.elems + + // ---- Error reporting TODO Once these are stabilized, move to messages -----" + + + def sharedPeaksStr(shared: Refs)(using Context): String = + shared.nth(0) match + case fresh @ root.Fresh(hidden) => + if hidden.owner.exists then i"$fresh of ${hidden.owner}" else i"$fresh" + case other => + i"$other" + + def overlapStr(hiddenSet: Refs, clashSet: Refs)(using Context): String = + val hiddenFootprint = hiddenSet.footprint() + val clashFootprint = clashSet.footprint() + // The overlap of footprints, or, of this empty the set of shared peaks. + // We prefer footprint overlap since it tends to be more informative. + val overlap = hiddenFootprint.overlapWith(clashFootprint) + if !overlap.isEmpty then i"${CaptureSet(overlap)}" + else + val sharedPeaks = hiddenSet.footprint(includeMax = true).sharedWith: + clashSet.footprint(includeMax = true) + assert(!sharedPeaks.isEmpty, i"no overlap for $hiddenSet vs $clashSet") + sharedPeaksStr(sharedPeaks) + + /** Report a separation failure in an application `fn(args)` + * @param fn the function + * @param parts the function prefix followed by the flattened argument list + * @param polyArg the clashing argument to a polymorphic formal + * @param clashing the argument, function prefix, or entire function application result with + * which it clashes, + * + */ + def sepApplyError(fn: Tree, parts: List[Tree], polyArg: Tree, clashing: Tree)(using Context): Unit = + val polyArgIdx = parts.indexOf(polyArg).ensuring(_ >= 0) - 1 + val clashIdx = parts.indexOf(clashing) // -1 means entire function application + def paramName(mt: Type, idx: Int): Option[Name] = mt match + case mt @ MethodType(pnames) => + if idx < pnames.length then Some(pnames(idx)) else paramName(mt.resType, idx - pnames.length) + case mt: PolyType => paramName(mt.resType, idx) + case _ => None + def formalName = paramName(fn.nuType.widen, polyArgIdx) match + case Some(pname) => i"$pname " + case _ => "" + def qualifier = methPart(fn) match + case Select(qual, _) => qual + case _ => EmptyTree + def isShowableMethod = fn.symbol.exists && !defn.isFunctionSymbol(fn.symbol.maybeOwner) + def funType = + if fn.symbol.exists && !qualifier.isEmpty then qualifier.nuType else fn.nuType + def funStr = + if isShowableMethod then i"${fn.symbol}: ${fn.symbol.info}" + else i"a function of type ${funType.widen}" + def clashArgStr = clashIdx match + case -1 => "function result" + case 0 => "function prefix" + case 1 => "first argument " + case 2 => "second argument" + case 3 => "third argument " + case n => s"${n}th argument " + def clashTypeStr = + if clashIdx == 0 && !isShowableMethod then "" // we already mentioned the type in `funStr` + else i" with type ${clashing.nuType}" + val hiddenSet = formalCaptures(polyArg).hiddenSet + val clashSet = captures(clashing) + report.error( + em"""Separation failure: argument of type ${polyArg.nuType} + |to $funStr + |corresponds to capture-polymorphic formal parameter ${formalName}of type ${polyArg.formalType} + |and hides capabilities ${CaptureSet(hiddenSet)}. + |Some of these overlap with the captures of the ${clashArgStr.trim}$clashTypeStr. + | + | Hidden set of current argument : ${CaptureSet(hiddenSet)} + | Hidden footprint of current argument : ${CaptureSet(hiddenSet.footprint())} + | Capture set of $clashArgStr : ${CaptureSet(clashSet)} + | Footprint set of $clashArgStr : ${CaptureSet(clashSet.footprint())} + | The two sets overlap at : ${overlapStr(hiddenSet, clashSet)}""", + polyArg.srcPos) + + /** Report a use/definition failure, where a previously hidden capability is + * used again. + * @param tree the tree where the capability is used + * @param clashing the tree where the capability is previously hidden, + * or emptyTree if none exists + * @param used the uses of `tree` + * @param hidden the hidden set of the clashing def, + * or the global hidden set if no clashing def exists + */ + def sepUseError(tree: Tree, clashingDef: ValOrDefDef | Null, used: Refs, hidden: Refs)(using Context): Unit = + if clashingDef != null then + def resultStr = if clashingDef.isInstanceOf[DefDef] then " result" else "" + report.error( + em"""Separation failure: Illegal access to ${overlapStr(hidden, used)} which is hidden by the previous definition + |of ${clashingDef.symbol} with$resultStr type ${clashingDef.tpt.nuType}. + |This type hides capabilities ${CaptureSet(hidden)}""", + tree.srcPos) + else + report.error( + em"""Separation failure: illegal access to ${overlapStr(hidden, used)} which is hidden by some previous definitions + |No clashing definitions were found. This might point to an internal error.""", + tree.srcPos) + + /** Report a failure where a previously consumed capability is used again, + * @param ref the capability that is used after being consumed + * @param loc the position where the capability was consumed + * @param pos the position where the capability was used again + */ + def consumeError(ref: CaptureRef, loc: SrcPos, pos: SrcPos)(using Context): Unit = + report.error( + em"""Separation failure: Illegal access to $ref, which was passed to a + |@consume parameter or was used as a prefix to a @consume method on line ${loc.line + 1} + |and therefore is no longer available.""", + pos) + + /** Report a failure where a capability is consumed in a loop. + * @param ref the capability + * @param loc the position where the capability was consumed + */ + def consumeInLoopError(ref: CaptureRef, pos: SrcPos)(using Context): Unit = + report.error( + em"""Separation failure: $ref appears in a loop, therefore it cannot + |be passed to a @consume parameter or be used as a prefix of a @consume method call.""", + pos) + + // ------------ Checks ----------------------------------------------------- + + /** Check separation between different arguments and between function + * prefix and arguments. A capability cannot be hidden by one of these arguments + * and also be either explicitly referenced or hidden by the prefix or another + * argument. "Hidden" means: the capability is in the deep capture set of the + * argument and appears in the hidden set of the corresponding (capture-polymorphic) + * formal parameter. Howeber, we do allow explicit references to a hidden + * capability in later arguments, if the corresponding formal parameter mentions + * the parameter where the capability was hidden. For instance in + * + * def seq(x: () => Unit; y ->{cap, x} Unit): Unit + * def f: () ->{io} Unit + * + * we do allow `seq(f, f)` even though `{f, io}` is in the hidden set of the + * first parameter `x`, since the second parameter explicitly mentions `x` in + * its capture set. + * + * Also check separation via checkType within individual arguments widened to their + * formal paramater types. + * + * @param fn the applied function + * @param args the flattened argument lists + * @param app the entire application tree + * @param deps cross argument dependencies: maps argument trees to + * those other arguments that where mentioned by coorresponding + * formal parameters. + */ + private def checkApply(fn: Tree, args: List[Tree], app: Tree, deps: collection.Map[Tree, List[Tree]])(using Context): Unit = + val (qual, fnCaptures) = methPart(fn) match + case Select(qual, _) => (qual, qual.nuType.captureSet) + case _ => (fn, CaptureSet.empty) + var currentPeaks = PeaksPair(fnCaptures.elems.peaks, emptyRefs) + val partsWithPeaks = mutable.ListBuffer[(Tree, PeaksPair)]() += (qual -> currentPeaks) + + capt.println( + i"""check separate $fn($args), fnCaptures = $fnCaptures, + | formalCaptures = ${args.map(arg => CaptureSet(formalCaptures(arg)))}, + | actualCaptures = ${args.map(arg => CaptureSet(captures(arg)))}, + | deps = ${deps.toList}""") + val parts = qual :: args + var reported: SimpleIdentitySet[Tree] = SimpleIdentitySet.empty + + for arg <- args do + val argPeaks = PeaksPair( + captures(arg).peaks, + if arg.needsSepCheck then formalCaptures(arg).hiddenSet.peaks else emptyRefs) + val argDeps = deps(arg) + + def clashingPart(argPeaks: Refs, selector: PeaksPair => Refs): Tree = + partsWithPeaks.find: (prev, prevPeaks) => + !argDeps.contains(prev) + && !selector(prevPeaks).sharedWith(argPeaks).isEmpty + match + case Some(prev, _) => prev + case None => EmptyTree + + // 1. test argPeaks.actual against previously captured hidden sets + if !argPeaks.actual.sharedWith(currentPeaks.hidden).isEmpty then + val clashing = clashingPart(argPeaks.actual, _.hidden) + if !clashing.isEmpty then + sepApplyError(fn, parts, clashing, arg) + reported += clashing + else assert(!argDeps.isEmpty) + + if arg.needsSepCheck then + //println(i"testing $arg, formal = ${arg.formalType}, peaks = ${argPeaks.actual}/${argPeaks.hidden} against ${currentPeaks.actual}") + checkType(arg.formalType, arg.srcPos, TypeRole.Argument(arg)) + // 2. test argPeaks.hidden against previously captured actuals + if !argPeaks.hidden.sharedWith(currentPeaks.actual).isEmpty then + val clashing = clashingPart(argPeaks.hidden, _.actual) + if !clashing.isEmpty then + if !reported.contains(clashing) then + //println(i"CLASH $arg / ${argPeaks.formal} vs $clashing / ${peaksOfTree(clashing).actual} / ${captures(clashing).peaks}") + sepApplyError(fn, parts, arg, clashing) + else assert(!argDeps.isEmpty) + + partsWithPeaks += (arg -> argPeaks) + currentPeaks = PeaksPair( + currentPeaks.actual ++ argPeaks.actual, + currentPeaks.hidden ++ argPeaks.hidden) + end for + + def collectRefs(args: List[Type], res: Type) = + args.foldLeft(argCaptures(res)): (refs, arg) => + refs ++ arg.deepCaptureSet.elems + + /** The deep capture sets of all parameters of this type (if it is a function type) */ + def argCaptures(tpe: Type): Refs = tpe match + case defn.FunctionOf(args, resultType, isContextual) => + collectRefs(args, resultType) + case defn.RefinedFunctionOf(mt) => + collectRefs(mt.paramInfos, mt.resType) + case CapturingType(parent, _) => + argCaptures(parent) + case _ => + emptyRefs + + if !deps(app).isEmpty then + lazy val appPeaks = argCaptures(app.nuType).peaks + lazy val partPeaks = partsWithPeaks.toMap + for arg <- deps(app) do + if arg.needsSepCheck && !partPeaks(arg).hidden.sharedWith(appPeaks).isEmpty then + sepApplyError(fn, parts, arg, app) + end checkApply + + /** 1. Check that the capabilities used at `tree` don't overlap with + * capabilities hidden by a previous definition. + * 2. Also check that none of the used capabilities was consumed before. + */ + def checkUse(tree: Tree)(using Context): Unit = + val used = tree.markedFree.elems + if !used.isEmpty then + capt.println(i"check use $tree: $used") + val usedPeaks = used.peaks + val overlap = defsShadow.peaks.sharedWith(usedPeaks) + if !defsShadow.peaks.sharedWith(usedPeaks).isEmpty then + val sym = tree.symbol + + def findClashing(prevDefs: List[DefInfo]): Option[DefInfo] = prevDefs match + case prevDef :: prevDefs1 => + if prevDef.symbol == sym then Some(prevDef) + else if !prevDef.hiddenPeaks.sharedWith(usedPeaks).isEmpty then Some(prevDef) + else findClashing(prevDefs1) + case Nil => + None + + findClashing(previousDefs) match + case Some(clashing) => + if clashing.symbol != sym then + sepUseError(tree, clashing.tree, used, clashing.hidden) + case None => + sepUseError(tree, null, used, defsShadow) + + for ref <- used do + val pos = consumed.clashing(ref) + if pos != null then consumeError(ref, pos, tree.srcPos) + end checkUse + + /** If `tp` denotes some version of a singleton capture ref `x.type` the set `{x, x*}` + * otherwise the empty set. + */ + def explicitRefs(tp: Type)(using Context): Refs = tp match + case tp: (TermRef | ThisType) if tp.isTrackableRef => SimpleIdentitySet(tp, tp.reach) + case AnnotatedType(parent, _) => explicitRefs(parent) + case AndType(tp1, tp2) => explicitRefs(tp1) ++ explicitRefs(tp2) + case OrType(tp1, tp2) => explicitRefs(tp1) ** explicitRefs(tp2) + case _ => emptyRefs + + /** Check validity of consumed references `refsToCheck`. The references are consumed + * because they are hidden in a Fresh result type or they are referred + * to in an argument to a @consume parameter or in a prefix of a @consume method -- + * which one applies is determined by the role parameter. + * + * This entails the following checks: + * - The reference must be defined in the same as method or class as + * the access. + * - If the reference is to a term parameter, that parameter must be + * marked as @consume as well. + * - If the reference is to a this type of the enclosing class, the + * access must be in a @consume method. + * + * References that extend SharedCapability are excluded from checking. + * As a side effect, add all checked references with the given position `pos` + * to the global `consumed` map. + * + * @param refsToCheck the referencves to check + * @param tpe the type containing those references + * @param role the role in which the type apears + * @param descr a textual description of the type and its relationship with the checked reference + * @param pos position for error reporting + */ + def checkConsumedRefs(refsToCheck: Refs, tpe: Type, role: TypeRole, descr: => String, pos: SrcPos)(using Context) = + val badParams = mutable.ListBuffer[Symbol]() + def currentOwner = role.dclSym.orElse(ctx.owner) + for hiddenRef <- refsToCheck.deductSymRefs(role.dclSym).deduct(explicitRefs(tpe)) do + if !hiddenRef.derivesFromSharedCapability then + hiddenRef.pathRoot match + case ref: TermRef => + val refSym = ref.symbol + if currentOwner.enclosingMethodOrClass.isProperlyContainedIn(refSym.maybeOwner.enclosingMethodOrClass) then + report.error(em"""Separation failure: $descr non-local $refSym""", pos) + else if refSym.is(TermParam) + && !refSym.hasAnnotation(defn.ConsumeAnnot) + && currentOwner.isContainedIn(refSym.owner) + then + badParams += refSym + case ref: ThisType => + val encl = currentOwner.enclosingMethodOrClass + if encl.isProperlyContainedIn(ref.cls) + && !encl.is(Synthetic) + && !encl.hasAnnotation(defn.ConsumeAnnot) + then + report.error( + em"""Separation failure: $descr non-local this of class ${ref.cls}. + |The access must be in a @consume method to allow this.""", + pos) + case _ => + + if badParams.nonEmpty then + def paramsStr(params: List[Symbol]): String = (params: @unchecked) match + case p :: Nil => i"${p.name}" + case p :: p2 :: Nil => i"${p.name} and ${p2.name}" + case p :: ps => i"${p.name}, ${paramsStr(ps)}" + val (pluralS, singleS) = if badParams.tail.isEmpty then ("", "s") else ("s", "") + report.error( + em"""Separation failure: $descr parameter$pluralS ${paramsStr(badParams.toList)}. + |The parameter$pluralS need$singleS to be annotated with @consume to allow this.""", + pos) + + role match + case _: TypeRole.Argument | _: TypeRole.Qualifier => + for ref <- refsToCheck do + if !ref.derivesFromSharedCapability then + consumed.put(ref, pos) + case _ => + end checkConsumedRefs + + /** Check separation conditions of type `tpe` that appears in `role`. + * 1. Check that the parts of type `tpe` are mutually separated, as defined in + * `checkParts` below. + * 2. Check that validity of all references consumed by the type as defined in + * `checkLegalRefs` below + */ + def checkType(tpe: Type, pos: SrcPos, role: TypeRole)(using Context): Unit = + + /** Deduct some elements from `refs` according to the role of the checked type `tpe`: + * - If the the type apears as a (result-) type of a definition of `x`, deduct + * `x` and `x*`. + * - If the checked type (or, for arguments, the actual type of the argument) + * is morally a singleton type `y.type` deduct `y` and `y*` as well. + */ + extension (refs: Refs) def pruned = + val deductedType = role match + case TypeRole.Argument(arg) => arg.tpe + case _ => tpe + refs.deductSymRefs(role.dclSym).deduct(explicitRefs(deductedType)) + + def sepTypeError(parts: List[Type], genPart: Type, otherPart: Type): Unit = + val captured = genPart.deepCaptureSet.elems + val hiddenSet = captured.hiddenSet.pruned + val clashSet = otherPart.deepCaptureSet.elems + val deepClashSet = (clashSet.footprint() ++ clashSet.hiddenSet).pruned + report.error( + em"""Separation failure in ${role.description} $tpe. + |One part, $genPart, hides capabilities ${CaptureSet(hiddenSet)}. + |Another part, $otherPart, captures capabilities ${CaptureSet(deepClashSet)}. + |The two sets overlap at ${overlapStr(hiddenSet, deepClashSet)}.""", + pos) + + /** Check that the parts of type `tpe` are mutually separated. + * This means that references hidden in some part of the type may not + * be explicitly referenced or hidden in some other part. + */ + def checkParts(parts: List[Type]): Unit = + var currentPeaks = PeaksPair(emptyRefs, emptyRefs) + val partsWithPeaks = mutable.ListBuffer[(Type, PeaksPair)]() + + for part <- parts do + val captured = part.deepCaptureSet.elems.pruned + val hidden = captured.hiddenSet.pruned + val actual = captured ++ hidden + val partPeaks = PeaksPair(actual.peaks, hidden.peaks) + /* + println(i"""check parts $parts + |current = ${currentPeaks.actual}/${currentPeaks.hidden} + |new = $captured/${captured.hiddenSet.pruned} + |new = ${captured.peaks}/${captured.hiddenSet.pruned.peaks}""") + */ + + def clashingPart(argPeaks: Refs, selector: PeaksPair => Refs): Type = + partsWithPeaks.find: (prev, prevPeaks) => + !selector(prevPeaks).sharedWith(argPeaks).isEmpty + match + case Some(prev, _) => prev + case None => NoType + + if !partPeaks.actual.sharedWith(currentPeaks.hidden).isEmpty then + //println(i"CLASH ${partPeaks.actual} with ${currentPeaks.hidden}") + val clashing = clashingPart(partPeaks.actual, _.hidden) + //println(i"CLASH ${partPeaks.actual} with ${currentPeaks.hidden}") + if clashing.exists then sepTypeError(parts, clashing, part) + + if !partPeaks.hidden.sharedWith(currentPeaks.actual).isEmpty then + val clashing = clashingPart(partPeaks.hidden, _.actual) + if clashing.exists then sepTypeError(parts, part, clashing) + + partsWithPeaks += (part -> partPeaks) + currentPeaks = PeaksPair( + currentPeaks.actual ++ partPeaks.actual, + currentPeaks.hidden ++ partPeaks.hidden) + end checkParts + + /** A traverser that collects part lists to check for separation conditions. + * The accumulator of type `Captures` indicates what kind of captures were + * encountered in previous parts. + */ + object traverse extends TypeAccumulator[Captures]: + + /** A stack of part lists to check. We maintain this since immediately + * checking parts when traversing the type would check innermost to outermost. + * But we want to check outermost parts first since this prioritizes errors + * that are more obvious. + */ + var toCheck: List[List[Type]] = Nil + + private val seen = util.HashSet[Symbol]() + + def apply(c: Captures, t: Type) = + if variance < 0 then c + else + val t1 = t.dealias + t1 match + case t @ AppliedType(tycon, args) => + val c1 = foldOver(Captures.None, t) + if c1 == Captures.NeedsCheck then + toCheck = (tycon :: args) :: toCheck + c.add(c1) + case t @ CapturingType(parent, cs) => + val c1 = this(c, parent) + if cs.elems.exists(_.stripReadOnly.isFresh) then c1.add(Captures.Hidden) + else if !cs.elems.isEmpty then c1.add(Captures.Explicit) + else c1 + case t: TypeRef if t.symbol.isAbstractOrParamType => + if seen.contains(t.symbol) then c + else + seen += t.symbol + apply(apply(c, t.prefix), t.info.bounds.hi) + case t => + foldOver(c, t) + + /** If `tpe` appears as a (result-) type of a definition, treat its + * hidden set minus its explicitly declared footprint as consumed. + * If `tpe` appears as an argument to a @consume parameter, treat + * its footprint as consumed. + */ + def checkLegalRefs() = role match + case TypeRole.Result(sym, _) => + if !sym.isAnonymousFunction // we don't check return types of anonymous functions + && !sym.is(Case) // We don't check so far binders in patterns since they + // have inferred universal types. TODO come back to this; + // either infer more precise types for such binders or + // "see through them" when we look at hidden sets. + then + val refs = tpe.deepCaptureSet.elems + val toCheck = refs.hiddenSet.footprint().deduct(refs.footprint()) + checkConsumedRefs(toCheck, tpe, role, i"${role.description} $tpe hides", pos) + case TypeRole.Argument(arg) => + if tpe.hasAnnotation(defn.ConsumeAnnot) then + val capts = captures(arg).footprint() + checkConsumedRefs(capts, tpe, role, i"argument to @consume parameter with type ${arg.nuType} refers to", pos) + case _ => + + if !tpe.hasAnnotation(defn.UntrackedCapturesAnnot) then + traverse(Captures.None, tpe) + traverse.toCheck.foreach(checkParts) + checkLegalRefs() + end checkType + + /** Check the (result-) type of a definition of symbol `sym` */ + def checkType(tpt: Tree, sym: Symbol)(using Context): Unit = + checkType(tpt.nuType, tpt.srcPos, + TypeRole.Result(sym, inferred = tpt.isInstanceOf[InferredTypeTree])) + + /** The list of all individual method types making up some potentially + * curried method type. + */ + private def collectMethodTypes(tp: Type): List[TermLambda] = tp match + case tp: MethodType => tp :: collectMethodTypes(tp.resType) + case tp: PolyType => collectMethodTypes(tp.resType) + case _ => Nil + + /** The inter-parameter dependencies of the function reference `fn` applied + * to the argument lists `argss`. For instance, if `f` has type + * + * f(x: A, y: B^{cap, x}, z: C^{x, y}): D + * + * then the dependencies of an application `f(a, b, c)` of type C^{y} is the map + * + * [ b -> [a] + * , c -> [a, b] + * , f(a, b, c) -> [b]] + */ + private def dependencies(fn: Tree, argss: List[List[Tree]], app: Tree)(using Context): collection.Map[Tree, List[Tree]] = + def isFunApply(sym: Symbol) = + sym.name == nme.apply && defn.isFunctionClass(sym.owner) + val mtpe = + if fn.symbol.exists && !isFunApply(fn.symbol) then fn.symbol.info + else fn.nuType.widen + val mtps = collectMethodTypes(mtpe) + assert(mtps.hasSameLengthAs(argss), i"diff for $fn: ${fn.symbol} /// $mtps /// $argss") + val mtpsWithArgs = mtps.zip(argss) + val argMap = mtpsWithArgs.toMap + val deps = mutable.HashMap[Tree, List[Tree]]().withDefaultValue(Nil) + + def recordDeps(formal: Type, actual: Tree) = + for dep <- formal.captureSet.elems.toList do + val referred = dep.stripReach match + case dep: TermParamRef => + argMap(dep.binder)(dep.paramNum) :: Nil + case dep: ThisType if dep.cls == fn.symbol.owner => + val Select(qual, _) = fn: @unchecked // TODO can we use fn instead? + qual :: Nil + case _ => + Nil + deps(actual) ++= referred + + for (mt, args) <- mtpsWithArgs; (formal, arg) <- mt.paramInfos.zip(args) do + recordDeps(formal, arg) + recordDeps(mtpe.finalResultType, app) + capt.println(i"deps for $app = ${deps.toList}") + deps + + + /** Decompose an application into a function prefix and a list of argument lists. + * If some of the arguments need a separation check because they are capture polymorphic, + * perform a separation check with `checkApply` + */ + private def traverseApply(app: Tree)(using Context): Unit = + def recur(tree: Tree, argss: List[List[Tree]]): Unit = tree match + case Apply(fn, args) => recur(fn, args :: argss) + case TypeApply(fn, args) => recur(fn, argss) // skip type arguments + case _ => + if argss.nestedExists(_.needsSepCheck) then + checkApply(tree, argss.flatten, app, dependencies(tree, argss, app)) + recur(app, Nil) + + /** Is `tree` an application of `caps.unsafe.unsafeAssumeSeparate`? */ + def isUnsafeAssumeSeparate(tree: Tree)(using Context): Boolean = tree match + case tree: Apply => tree.symbol == defn.Caps_unsafeAssumeSeparate + case _ => false + + def pushDef(tree: ValOrDefDef, hiddenByDef: Refs)(using Context): Unit = + defsShadow ++= hiddenByDef + previousDefs = DefInfo(tree, tree.symbol, hiddenByDef, hiddenByDef.peaks) :: previousDefs + + /** Check (result-) type of `tree` for separation conditions using `checkType`. + * Excluded are parameters and definitions that have an =unsafeAssumeSeparate + * application as right hand sides. + * Hidden sets of checked definitions are added to `defsShadow`. + */ + def checkValOrDefDef(tree: ValOrDefDef)(using Context): Unit = + if !tree.symbol.isOneOf(TermParamOrAccessor) && !isUnsafeAssumeSeparate(tree.rhs) then + checkType(tree.tpt, tree.symbol) + capt.println(i"sep check def ${tree.symbol}: ${tree.tpt} with ${captures(tree.tpt).hiddenSet.footprint()}") + pushDef(tree, captures(tree.tpt).hiddenSet.deductSymRefs(tree.symbol)) + + def inSection[T](op: => T)(using Context): T = + val savedDefsShadow = defsShadow + val savedPrevionsDefs = previousDefs + try op + finally + previousDefs = savedPrevionsDefs + defsShadow = savedDefsShadow + + def traverseSection[T](tree: Tree)(using Context) = inSection(traverseChildren(tree)) + + /** Traverse `tree` and perform separation checks everywhere */ + def traverse(tree: Tree)(using Context): Unit = + if !isUnsafeAssumeSeparate(tree) then trace(i"checking separate $tree"): + checkUse(tree) + tree match + case tree @ Select(qual, _) if tree.symbol.is(Method) && tree.symbol.hasAnnotation(defn.ConsumeAnnot) => + traverseChildren(tree) + checkConsumedRefs( + captures(qual).footprint(), qual.nuType, + TypeRole.Qualifier(qual, tree.symbol), + i"call prefix of @consume ${tree.symbol} refers to", qual.srcPos) + case tree: GenericApply => + traverseChildren(tree) + tree.tpe match + case _: MethodOrPoly => + case _ => traverseApply(tree) + case _: Block | _: Template => + traverseSection(tree) + case tree: ValDef => + traverseChildren(tree) + checkValOrDefDef(tree) + case tree: DefDef => + inSection: + consumed.segment: + for params <- tree.paramss; case param: ValDef <- params do + pushDef(param, emptyRefs) + traverseChildren(tree) + checkValOrDefDef(tree) + case If(cond, thenp, elsep) => + traverse(cond) + val thenConsumed = consumed.segment(traverse(thenp)) + val elseConsumed = consumed.segment(traverse(elsep)) + consumed ++= thenConsumed + consumed ++= elseConsumed + case tree @ Labeled(bind, expr) => + val consumedBuf = mutable.ListBuffer[ConsumedSet]() + openLabeled = (bind.name, consumedBuf) :: openLabeled + traverse(expr) + for cs <- consumedBuf do consumed ++= cs + openLabeled = openLabeled.tail + case Return(expr, from) => + val retConsumed = consumed.segment(traverse(expr)) + from match + case Ident(name) => + for (lbl, consumedBuf) <- openLabeled do + if lbl == name then + consumedBuf += retConsumed + case _ => + case Match(sel, cases) => + // Matches without returns might still be kept after pattern matching to + // encode table switches. + traverse(sel) + val caseConsumed = for cas <- cases yield consumed.segment(traverse(cas)) + caseConsumed.foreach(consumed ++= _) + case tree: TypeDef if tree.symbol.isClass => + consumed.segment: + traverseChildren(tree) + case tree: WhileDo => + val loopConsumed = consumed.segment(traverseChildren(tree)) + if loopConsumed.size != 0 then + val (ref, pos) = loopConsumed.toMap.head + consumeInLoopError(ref, pos) + case _ => + traverseChildren(tree) +end SepCheck \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index a5e96f1f9ce2..21c58385b58a 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -11,15 +11,16 @@ import config.Feature import config.Printers.{capt, captDebug} import ast.tpd, tpd.* import transform.{PreRecheck, Recheck}, Recheck.* -import CaptureSet.{IdentityCaptRefMap, IdempotentCaptRefMap} import Synthetics.isExcluded import util.SimpleIdentitySet +import util.chaining.* import reporting.Message import printing.{Printer, Texts}, Texts.{Text, Str} import collection.mutable import CCState.* import dotty.tools.dotc.util.NoSourcePosition import CheckCaptures.CheckerAPI +import NamerOps.methodType /** Operations accessed from CheckCaptures */ trait SetupAPI: @@ -85,7 +86,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: /** Drops `private` from the flags of `symd` provided it is * a parameter accessor that's not `constructorOnly` or `uncheckedCaptured` * and that contains at least one @retains in co- or in-variant position. - * The @retains mught be implicit for a type deriving from `Capability`. + * The @retains might be implicit for a type deriving from `Capability`. */ private def newFlagsFor(symd: SymDenotation)(using Context): FlagSet = @@ -94,12 +95,13 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def apply(x: Boolean, tp: Type): Boolean = if x then true else if tp.derivesFromCapability && variance >= 0 then true - else tp match + else tp.dealiasKeepAnnots match case AnnotatedType(_, ann) if ann.symbol.isRetains && variance >= 0 => true case t: TypeRef if t.symbol.isAbstractOrParamType && !seen.contains(t.symbol) => seen += t.symbol apply(x, t.info.bounds.hi) - case _ => foldOver(x, tp) + case tp1 => + foldOver(x, tp1) def apply(tp: Type): Boolean = apply(false, tp) if symd.symbol.isRefiningParamAccessor @@ -132,7 +134,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def mappedInfo = if toBeUpdated.contains(sym) then symd.info // don't transform symbols that will anyway be updated - else transformExplicitType(symd.info) + else transformExplicitType(symd.info, sym, freshen = true) if Synthetics.needsTransform(symd) then Synthetics.transform(symd, mappedInfo) else if isPreCC(sym) then @@ -180,6 +182,75 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tp: MethodOrPoly => tp // don't box results of methods outside refinements case _ => recur(tp) + private trait SetupTypeMap extends FollowAliasesMap: + private var isTopLevel = true + + protected def innerApply(tp: Type): Type + + final def apply(tp: Type) = + val saved = isTopLevel + if variance < 0 then isTopLevel = false + try tp match + case defn.RefinedFunctionOf(rinfo: MethodType) => + val rinfo1 = apply(rinfo) + if rinfo1 ne rinfo then rinfo1.toFunctionType(alwaysDependent = true) + else tp + case _ => + innerApply(tp) + finally isTopLevel = saved + + /** Map parametric functions with results that have a capture set somewhere + * to dependent functions. + */ + protected def normalizeFunctions(tp: Type, original: Type, expandAlways: Boolean = false)(using Context): Type = + tp match + case AppliedType(tycon, args) + if defn.isNonRefinedFunction(tp) && isTopLevel => + // Expand if we have an applied type that underwent some addition of capture sets + val expand = expandAlways || original.match + case AppliedType(`tycon`, args0) => args0.last ne args.last + case _ => false + if expand then + depFun(args.init, args.last, + isContextual = defn.isContextFunctionClass(tycon.classSymbol)) + .showing(i"add function refinement $tp ($tycon, ${args.init}, ${args.last}) --> $result", capt) + else tp + case _ => tp + + /** Pull out an embedded capture set from a part of `tp` */ + def normalizeCaptures(tp: Type)(using Context): Type = tp match + case tp @ RefinedType(parent @ CapturingType(parent1, refs), rname, rinfo) => + CapturingType(tp.derivedRefinedType(parent1, rname, rinfo), refs, parent.isBoxed) + case tp: RecType => + tp.parent match + case parent @ CapturingType(parent1, refs) => + CapturingType(tp.derivedRecType(parent1), refs, parent.isBoxed) + case _ => + tp // can return `tp` here since unlike RefinedTypes, RecTypes are never created + // by `mapInferred`. Hence if the underlying type admits capture variables + // a variable was already added, and the first case above would apply. + case AndType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) => + assert(tp1.isBoxed == tp2.isBoxed) + CapturingType(AndType(parent1, parent2), refs1 ** refs2, tp1.isBoxed) + case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) => + assert(tp1.isBoxed == tp2.isBoxed) + CapturingType(OrType(parent1, parent2, tp.isSoft), refs1 ++ refs2, tp1.isBoxed) + case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2) => + CapturingType(OrType(parent1, tp2, tp.isSoft), refs1, tp1.isBoxed) + case tp @ OrType(tp1, tp2 @ CapturingType(parent2, refs2)) => + CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, tp2.isBoxed) + case tp @ AppliedType(tycon, args) + if !defn.isFunctionClass(tp.dealias.typeSymbol) && (tp.dealias eq tp) => + tp.derivedAppliedType(tycon, args.mapConserve(box)) + case tp: RealTypeBounds => + tp.derivedTypeBounds(tp.lo, box(tp.hi)) + case tp: LazyRef => + normalizeCaptures(tp.ref) + case _ => + tp + + end SetupTypeMap + /** Transform the type of an InferredTypeTree by performing the following transformation * steps everywhere in the type: * 1. Drop retains annotations @@ -197,9 +268,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * Polytype bounds are only cleaned using step 1, but not otherwise transformed. */ private def transformInferredType(tp: Type)(using Context): Type = - def mapInferred(refine: Boolean): TypeMap = new TypeMap with FollowAliasesMap: + def mapInferred(refine: Boolean): TypeMap = new TypeMap with SetupTypeMap: override def toString = "map inferred" + var refiningNames: Set[Name] = Set() + /** Refine a possibly applied class type C where the class has tracked parameters * x_1: T_1, ..., x_n: T_n to C { val x_1: T_1^{CV_1}, ..., val x_n: T_n^{CV_n} } * where CV_1, ..., CV_n are fresh capture set variables. @@ -212,7 +285,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: cls.paramGetters.foldLeft(tp) { (core, getter) => if atPhase(thisPhase.next)(getter.hasTrackedParts) && getter.isRefiningParamAccessor - && !getter.is(Tracked) + && !refiningNames.contains(getter.name) // Don't add a refinement if we have already an explicit one for the same name then val getterType = mapInferred(refine = false)(tp.memberInfo(getter)).strippedDealias @@ -225,67 +298,32 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case _ => tp case _ => tp - private var isTopLevel = true - - private def mapNested(ts: List[Type]): List[Type] = - val saved = isTopLevel - isTopLevel = false - try ts.mapConserve(this) - finally isTopLevel = saved - - def apply(tp: Type) = + def innerApply(tp: Type) = val tp1 = tp match case AnnotatedType(parent, annot) if annot.symbol.isRetains => // Drop explicit retains annotations apply(parent) - case tp @ AppliedType(tycon, args) => - val tycon1 = this(tycon) - if defn.isNonRefinedFunction(tp) then - // Convert toplevel generic function types to dependent functions - if !defn.isFunctionSymbol(tp.typeSymbol) && (tp.dealias ne tp) then - // This type is a function after dealiasing, so we dealias and recurse. - // See #15925. - this(tp.dealias) - else - val args0 = args.init - var res0 = args.last - val args1 = mapNested(args0) - val res1 = this(res0) - if isTopLevel then - depFun(args1, res1, - isContextual = defn.isContextFunctionClass(tycon1.classSymbol)) - .showing(i"add function refinement $tp ($tycon1, $args1, $res1) (${tp.dealias}) --> $result", capt) - else if (tycon1 eq tycon) && (args1 eq args0) && (res1 eq res0) then - tp - else - tp.derivedAppliedType(tycon1, args1 :+ res1) - else - tp.derivedAppliedType(tycon1, args.mapConserve(arg => box(this(arg)))) - case defn.RefinedFunctionOf(rinfo: MethodType) => - val rinfo1 = apply(rinfo) - if rinfo1 ne rinfo then rinfo1.toFunctionType(alwaysDependent = true) - else tp - case Existential(_, unpacked) => - // drop the existential, the bound variables will be replaced by capture set variables - apply(unpacked) - case tp: MethodType => - tp.derivedLambdaType( - paramInfos = mapNested(tp.paramInfos), - resType = this(tp.resType)) case tp: TypeLambda => // Don't recurse into parameter bounds, just cleanup any stray retains annotations - tp.derivedLambdaType( - paramInfos = tp.paramInfos.mapConserve(_.dropAllRetains.bounds), - resType = this(tp.resType)) + ccState.withoutMappedFutureElems: + tp.derivedLambdaType( + paramInfos = tp.paramInfos.mapConserve(_.dropAllRetains.bounds), + resType = this(tp.resType)) + case tp @ RefinedType(parent, rname, rinfo) => + val saved = refiningNames + refiningNames += rname + val parent1 = try this(parent) finally refiningNames = saved + tp.derivedRefinedType(parent1, rname, this(rinfo)) case _ => mapFollowingAliases(tp) - addVar(addCaptureRefinements(normalizeCaptures(tp1)), ctx.owner) - end apply + addVar( + addCaptureRefinements(normalizeCaptures(normalizeFunctions(tp1, tp))), + ctx.owner) end mapInferred try val tp1 = mapInferred(refine = true)(tp) - val tp2 = Existential.mapCapInResults(_ => assert(false))(tp1) + val tp2 = root.toResultInResults(_ => assert(false))(tp1) if tp2 ne tp then capt.println(i"expanded inferred in ${ctx.owner}: $tp --> $tp1 --> $tp2") tp2 catch case ex: AssertionError => @@ -300,11 +338,32 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * 3. Add universal capture sets to types deriving from Capability * 4. Map `cap` in function result types to existentially bound variables. * 5. Schedule deferred well-formed tests for types with retains annotations. - * 6. Perform normalizeCaptures + * 6. Perform normalizeCaptures */ - private def transformExplicitType(tp: Type, tptToCheck: Tree = EmptyTree)(using Context): Type = - val toCapturing = new DeepTypeMap with FollowAliasesMap: - override def toString = "expand aliases" + private def transformExplicitType(tp: Type, sym: Symbol, freshen: Boolean, tptToCheck: Tree = EmptyTree)(using Context): Type = + + def fail(msg: Message) = + if !tptToCheck.isEmpty then report.error(msg, tptToCheck.srcPos) + + /** If C derives from Capability and we have a C^cs in source, we leave it as is + * instead of expanding it to C^{cap.rd}^cs. We do this by stripping capability-generated + * universal capture sets from the parent of a CapturingType. + */ + def stripImpliedCaptureSet(tp: Type): Type = tp match + case tp @ CapturingType(parent, refs) + if (refs eq CaptureSet.universalImpliedByCapability) && !tp.isBoxedCapturing => + parent + case tp: AliasingBounds => + tp.derivedAlias(stripImpliedCaptureSet(tp.alias)) + case tp: RealTypeBounds => + tp.derivedTypeBounds(stripImpliedCaptureSet(tp.lo), stripImpliedCaptureSet(tp.hi)) + case _ => tp + + object toCapturing extends DeepTypeMap, SetupTypeMap: + override def toString = "transformExplicitType" + + var keepFunAliases = true + var keptFunAliases = false /** Expand $throws aliases. This is hard-coded here since $throws aliases in stdlib * are defined with `?=>` rather than `?->`. @@ -331,20 +390,38 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: CapturingType(fntpe, cs, boxed = false) else fntpe - /** If C derives from Capability and we have a C^cs in source, we leave it as is - * instead of expanding it to C^{cap}^cs. We do this by stripping capability-generated - * universal capture sets from the parent of a CapturingType. + /** Check that types extending SharedCapability don't have a `cap` in their capture set. + * TODO This is not enough. + * We need to also track that we cannot get exclusive capabilities in paths + * where some prefix derives from SharedCapability. Also, can we just + * exclude `cap`, or do we have to extend this to all exclusive capabilties? + * The problem is that we know what is exclusive in general only after capture + * checking, not before. */ - def stripImpliedCaptureSet(tp: Type): Type = tp match - case tp @ CapturingType(parent, refs) - if (refs eq defn.universalCSImpliedByCapability) && !tp.isBoxedCapturing => - parent - case _ => tp + def checkSharedOK(tp: Type): tp.type = + tp match + case CapturingType(parent, refs) + if refs.isUniversal && parent.derivesFromSharedCapability => + fail(em"$tp extends SharedCapability, so it cannot capture `cap`") + case _ => + tp - def apply(t: Type) = + /** Map references to capability classes C to C^, + * normalize captures and map to dependent functions. + */ + def defaultApply(t: Type) = + if t.derivesFromCapability + && !t.isSingleton + && (!sym.isConstructor || (t ne tp.finalResultType)) + // Don't add ^ to result types of class constructors deriving from Capability + then CapturingType(t, CaptureSet.universalImpliedByCapability, boxed = false) + else normalizeCaptures(mapFollowingAliases(t)) + + def innerApply(t: Type) = t match case t @ CapturingType(parent, refs) => - t.derivedCapturingType(stripImpliedCaptureSet(this(parent)), refs) + checkSharedOK: + t.derivedCapturingType(stripImpliedCaptureSet(this(parent)), refs) case t @ AnnotatedType(parent, ann) => val parent1 = this(parent) if ann.symbol.isRetains then @@ -352,68 +429,53 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if !tptToCheck.isEmpty then checkWellformedLater(parent2, ann.tree, tptToCheck) try - CapturingType(parent2, ann.tree.toCaptureSet) + checkSharedOK: + CapturingType(parent2, ann.tree.toCaptureSet) catch case ex: IllegalCaptureRef => - report.error(em"Illegal capture reference: ${ex.getMessage.nn}", tptToCheck.srcPos) + if !tptToCheck.isEmpty then + report.error(em"Illegal capture reference: ${ex.getMessage.nn}", tptToCheck.srcPos) parent2 + else if ann.symbol == defn.UncheckedCapturesAnnot then + makeUnchecked(apply(parent)) else t.derivedAnnotatedType(parent1, ann) case throwsAlias(res, exc) => this(expandThrowsAlias(res, exc, Nil)) + case t @ AppliedType(tycon, args) + if defn.isNonRefinedFunction(t) + && !defn.isFunctionSymbol(t.typeSymbol) && (t.dealias ne tp) => + if keepFunAliases then + // Hold off with dealising and expand in a second pass. + // This is necessary to bind existentialFresh instances to the right method binder. + keptFunAliases = true + mapOver(t) + else + // In the second pass, map the alias + apply(t.dealias) case t => - // Map references to capability classes C to C^ - if t.derivesFromCapability && !t.isSingleton && t.typeSymbol != defn.Caps_Exists - then CapturingType(t, defn.universalCSImpliedByCapability, boxed = false) - else normalizeCaptures(mapFollowingAliases(t)) + defaultApply(t) end toCapturing - def fail(msg: Message) = - if !tptToCheck.isEmpty then report.error(msg, tptToCheck.srcPos) + def transform(tp: Type): Type = + val tp1 = toCapturing(tp) + val tp2 = root.toResultInResults(fail, toCapturing.keepFunAliases)(tp1) + val snd = if toCapturing.keepFunAliases then "" else " 2nd time" + if tp2 ne tp then capt.println(i"expanded explicit$snd in ${ctx.owner}: $tp --> $tp1 --> $tp2") + tp2 - val tp1 = toCapturing(tp) - val tp2 = Existential.mapCapInResults(fail)(tp1) - if tp2 ne tp then capt.println(i"expanded explicit in ${ctx.owner}: $tp --> $tp1 --> $tp2") - tp2 + val tp1 = transform(tp) + val tp2 = + if toCapturing.keptFunAliases then + toCapturing.keepFunAliases = false + transform(tp1) + else tp1 + val tp3 = + if sym.isType then stripImpliedCaptureSet(tp2) + else tp2 + if freshen then root.capToFresh(tp3).tap(addOwnerAsHidden(_, sym)) + else tp3 end transformExplicitType - /** Substitute parameter symbols in `from` to paramRefs in corresponding - * method or poly types `to`. We use a single BiTypeMap to do everything. - * @param from a list of lists of type or term parameter symbols of a curried method - * @param to a list of method or poly types corresponding one-to-one to the parameter lists - */ - private class SubstParams(from: List[List[Symbol]], to: List[LambdaType])(using Context) - extends DeepTypeMap, BiTypeMap: - - def apply(t: Type): Type = t match - case t: NamedType => - if t.prefix == NoPrefix then - val sym = t.symbol - def outer(froms: List[List[Symbol]], tos: List[LambdaType]): Type = - def inner(from: List[Symbol], to: List[ParamRef]): Type = - if from.isEmpty then outer(froms.tail, tos.tail) - else if sym eq from.head then to.head - else inner(from.tail, to.tail) - if tos.isEmpty then t - else inner(froms.head, tos.head.paramRefs) - outer(from, to) - else t.derivedSelect(apply(t.prefix)) - case _ => - mapOver(t) - - lazy val inverse = new BiTypeMap: - override def toString = "SubstParams.inverse" - def apply(t: Type): Type = t match - case t: ParamRef => - def recur(from: List[LambdaType], to: List[List[Symbol]]): Type = - if from.isEmpty then t - else if t.binder eq from.head then to.head(t.paramNum).namedType - else recur(from.tail, to.tail) - recur(to, from) - case _ => - mapOver(t) - def inverse = SubstParams.this - end SubstParams - /** Update info of `sym` for CheckCaptures phase only */ private def updateInfo(sym: Symbol, info: Type)(using Context) = toBeUpdated += sym @@ -424,31 +486,45 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: extension (sym: Symbol) def nextInfo(using Context): Type = atPhase(thisPhase.next)(sym.info) + private def addOwnerAsHidden(tp: Type, owner: Symbol)(using Context): Unit = + val ref = owner.termRef + def add = new TypeTraverser: + var reach = false + def traverse(t: Type): Unit = t match + case root.Fresh(hidden) => + if reach then hidden.elems += ref.reach + else if ref.isTracked then hidden.elems += ref + case t @ CapturingType(_, _) if t.isBoxed && !reach => + reach = true + try traverseChildren(t) finally reach = false + case _ => + traverseChildren(t) + if ref.isTrackableRef then add.traverse(tp) + end addOwnerAsHidden + /** A traverser that adds knownTypes and updates symbol infos */ def setupTraverser(checker: CheckerAPI) = new TreeTraverserWithPreciseImportContexts: import checker.* - /** Transform type of tree, and remember the transformed type as the type the tree */ - private def transformTT(tree: TypeTree, boxed: Boolean)(using Context): Unit = + /** Transform type of tree, and remember the transformed type as the type of the tree + * @pre !(boxed && sym.exists) + */ + private def transformTT(tree: TypeTree, sym: Symbol, boxed: Boolean)(using Context): Unit = if !tree.hasNuType then - val transformed = + var transformed = if tree.isInferred then transformInferredType(tree.tpe) - else transformExplicitType(tree.tpe, tptToCheck = tree) - tree.setNuType(if boxed then box(transformed) else transformed) + else transformExplicitType(tree.tpe, sym, freshen = !boxed, tptToCheck = tree) + if boxed then transformed = box(transformed) + tree.setNuType( + if sym.hasAnnotation(defn.UncheckedCapturesAnnot) then makeUnchecked(transformed) + else transformed) /** Transform the type of a val or var or the result type of a def */ def transformResultType(tpt: TypeTree, sym: Symbol)(using Context): Unit = // First step: Transform the type and record it as knownType of tpt. try - transformTT(tpt, - boxed = - sym.is(Mutable, butNot = Method) - && !ccConfig.useSealed - && !sym.hasAnnotation(defn.UncheckedCapturesAnnot), - // Under the sealed policy, we disallow root capabilities in the type of mutable - // variables, no need to box them here. - ) + transformTT(tpt, sym, boxed = false) catch case ex: IllegalCaptureRef => capt.println(i"fail while transforming result type $tpt of $sym") throw ex @@ -473,8 +549,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if isExcluded(meth) then return - meth.recordLevel() - inNestedLevel: + ccState.recordLevel(meth) + ccState.inNestedLevel: inContext(ctx.withOwner(meth)): paramss.foreach(traverse) transformResultType(tpt, meth) @@ -482,7 +558,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tree @ ValDef(_, tpt: TypeTree, _) => val sym = tree.symbol - sym.recordLevel() + ccState.recordLevel(sym) val defCtx = if sym.isOneOf(TermParamOrAccessor) then ctx else ctx.withOwner(sym) inContext(defCtx): transformResultType(tpt, sym) @@ -490,34 +566,39 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tree @ TypeApply(fn, args) => traverse(fn) - if !defn.isTypeTestOrCast(fn.symbol) then - for case arg: TypeTree <- args do - transformTT(arg, boxed = true) // type arguments in type applications are boxed + for case arg: TypeTree <- args do + if defn.isTypeTestOrCast(fn.symbol) then + arg.setNuType(root.capToFresh(arg.tpe)) + else + transformTT(arg, NoSymbol, boxed = true) // type arguments in type applications are boxed case tree: TypeDef if tree.symbol.isClass => val sym = tree.symbol - sym.recordLevel() - inNestedLevelUnless(sym.is(Module)): + ccState.recordLevel(sym) + ccState.inNestedLevelUnless(sym.is(Module)): inContext(ctx.withOwner(sym)) traverseChildren(tree) + case tree @ TypeDef(_, rhs: TypeTree) => + transformTT(rhs, tree.symbol, boxed = false) + case tree @ SeqLiteral(elems, tpt: TypeTree) => traverse(elems) tpt.setNuType(box(transformInferredType(tpt.tpe))) case tree: Block => - inNestedLevel(traverseChildren(tree)) + ccState.inNestedLevel(traverseChildren(tree)) case _ => traverseChildren(tree) postProcess(tree) - checkProperUse(tree) + checkProperUseOrConsume(tree) end traverse /** Processing done on node `tree` after its children are traversed */ def postProcess(tree: Tree)(using Context): Unit = tree match case tree: TypeTree => - transformTT(tree, boxed = false) + transformTT(tree, NoSymbol, boxed = false) case tree: ValOrDefDef => // Make sure denotation of tree's symbol is correct val sym = tree.symbol @@ -539,84 +620,57 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: else tree.tpt.nuType // A test whether parameter signature might change. This returns true if one of - // the parameters has a new type installee. The idea here is that we store a new + // the parameters has a new type installed. The idea here is that we store a new // type only if the transformed type is different from the original. def paramSignatureChanges = tree.match case tree: DefDef => tree.paramss.nestedExists: - case param: ValDef => param.tpt.hasNuType + case param: ValDef => param.tpt.hasNuType case param: TypeDef => param.rhs.hasNuType case _ => false // A symbol's signature changes if some of its parameter types or its result type // have a new type installed here (meaning hasRememberedType is true) def signatureChanges = - tree.tpt.hasNuType && !sym.isConstructor || paramSignatureChanges - - // Replace an existing symbol info with inferred types where capture sets of - // TypeParamRefs and TermParamRefs are put in correspondence by BiTypeMaps with the - // capture sets of the types of the method's parameter symbols and result type. - def integrateRT( - info: Type, // symbol info to replace - psymss: List[List[Symbol]], // the local (type and term) parameter symbols corresponding to `info` - resType: Type, // the locally computed return type - prevPsymss: List[List[Symbol]], // the local parameter symbols seen previously in reverse order - prevLambdas: List[LambdaType] // the outer method and polytypes generated previously in reverse order - ): Type = - info match - case mt: MethodOrPoly => - val psyms = psymss.head - // TODO: the substitution does not work for param-dependent method types. - // For example, `(x: T, y: x.f.type) => Unit`. In this case, when we - // substitute `x.f.type`, `x` becomes a `TermParamRef`. But the new method - // type is still under initialization and `paramInfos` is still `null`, - // so the new `NamedType` will not have a denotation. - def adaptedInfo(psym: Symbol, info: mt.PInfo): mt.PInfo = mt.companion match - case mtc: MethodTypeCompanion => mtc.adaptParamInfo(psym, info).asInstanceOf[mt.PInfo] - case _ => info - mt.companion(mt.paramNames)( - mt1 => - if !paramSignatureChanges && !mt.isParamDependent && prevLambdas.isEmpty then - mt.paramInfos - else - val subst = SubstParams(psyms :: prevPsymss, mt1 :: prevLambdas) - psyms.map(psym => adaptedInfo(psym, subst(psym.nextInfo).asInstanceOf[mt.PInfo])), - mt1 => - integrateRT(mt.resType, psymss.tail, resType, psyms :: prevPsymss, mt1 :: prevLambdas) - ) - case info: ExprType => - info.derivedExprType(resType = - integrateRT(info.resType, psymss, resType, prevPsymss, prevLambdas)) - case info => - if prevLambdas.isEmpty then resType - else SubstParams(prevPsymss, prevLambdas)(resType) + tree.tpt.hasNuType || paramSignatureChanges + + def paramsToCap(mt: Type)(using Context): Type = mt match + case mt: MethodType => + mt.derivedLambdaType( + paramInfos = mt.paramInfos.map(root.freshToCap), + resType = paramsToCap(mt.resType)) + case mt: PolyType => + mt.derivedLambdaType(resType = paramsToCap(mt.resType)) + case _ => mt // If there's a change in the signature, update the info of `sym` if sym.exists && signatureChanges then - val newInfo = - Existential.mapCapInResults(report.error(_, tree.srcPos)): - integrateRT(sym.info, sym.paramSymss, localReturnType, Nil, Nil) - .showing(i"update info $sym: ${sym.info} = $result", capt) - if newInfo ne sym.info then - val updatedInfo = - if sym.isAnonymousFunction - || sym.is(Param) - || sym.is(ParamAccessor) - || sym.isPrimaryConstructor - then - // closures are handled specially; the newInfo is constrained from - // the expected type and only afterwards we recheck the definition - newInfo - else new LazyType: - // infos of other methods are determined from their definitions, which - // are checked on demand + val updatedInfo = + + val paramSymss = sym.paramSymss + def newInfo(using Context) = // will be run in this or next phase + root.toResultInResults(report.error(_, tree.srcPos)): + if sym.is(Method) then + paramsToCap(methodType(paramSymss, localReturnType)) + else tree.tpt.nuType + if tree.tpt.isInstanceOf[InferredTypeTree] + && !sym.is(Param) && !sym.is(ParamAccessor) + then + val prevInfo = sym.info + new LazyType: def complete(denot: SymDenotation)(using Context) = assert(ctx.phase == thisPhase.next, i"$sym") - capt.println(i"forcing $sym, printing = ${ctx.mode.is(Mode.Printing)}") - //if ctx.mode.is(Mode.Printing) then new Error().printStackTrace() - denot.info = newInfo - completeDef(tree, sym) - updateInfo(sym, updatedInfo) + sym.info = prevInfo // set info provisionally so we can analyze the symbol in recheck + completeDef(tree, sym, this) + sym.info = newInfo + .showing(i"new info of $sym = $result", capt) + else if sym.is(Method) then + new LazyType: + def complete(denot: SymDenotation)(using Context) = + sym.info = newInfo + .showing(i"new info of $sym = $result", capt) + else newInfo + updateInfo(sym, updatedInfo) case tree: Bind => val sym = tree.symbol @@ -624,7 +678,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tree: TypeDef => tree.symbol match case cls: ClassSymbol => - inNestedLevelUnless(cls.is(Module)): + ccState.inNestedLevelUnless(cls.is(Module)): val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo // Compute new self type @@ -644,11 +698,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: // Infer the self type for the rest, which is all classes without explicit // self types (to which we also add nested module classes), provided they are // neither pure, nor are publicily extensible with an unconstrained self type. - CapturingType(cinfo.selfType, CaptureSet.Var(cls, level = currentLevel)) + CapturingType(cinfo.selfType, CaptureSet.Var(cls, level = ccState.currentLevel)) // Compute new parent types val ps1 = inContext(ctx.withOwner(cls)): - ps.mapConserve(transformExplicitType(_)) + ps.mapConserve(transformExplicitType(_, NoSymbol, freshen = false)) // Install new types and if it is a module class also update module object if (selfInfo1 ne selfInfo) || (ps1 ne ps) then @@ -659,22 +713,44 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if cls.is(ModuleClass) then // if it's a module, the capture set of the module reference is the capture set of the self type val modul = cls.sourceModule - updateInfo(modul, CapturingType(modul.info, selfInfo1.asInstanceOf[Type].captureSet)) + val selfCaptures = selfInfo1 match + case CapturingType(_, refs) => refs + case _ => CaptureSet.empty + // Note: Can't do val selfCaptures = selfInfo1.captureSet here. + // This would potentially give stackoverflows when setup is run repeatedly. + // One test case is pos-custom-args/captures/checkbounds.scala under + // ccConfig.alwaysRepeatRun = true. + updateInfo(modul, CapturingType(modul.info, selfCaptures)) modul.termRef.invalidateCaches() case _ => case _ => end postProcess - /** Check that @use annotations only appear on parameters and not on anonymous function parameters */ - def checkProperUse(tree: Tree)(using Context): Unit = tree match + /** Check that @use and @consume annotations only appear on parameters and not on + * anonymous function parameters + */ + def checkProperUseOrConsume(tree: Tree)(using Context): Unit = tree match case tree: MemberDef => - def useAllowed(sym: Symbol) = - (sym.is(Param) || sym.is(ParamAccessor)) && !sym.owner.isAnonymousFunction + val sym = tree.symbol + def isMethodParam = (sym.is(Param) || sym.is(ParamAccessor)) + && !sym.owner.isAnonymousFunction for ann <- tree.symbol.annotations do - if ann.symbol == defn.UseAnnot && !useAllowed(tree.symbol) then - report.error(i"Only parameters of methods can have @use annotations", tree.srcPos) + val annotCls = ann.symbol + if annotCls == defn.ConsumeAnnot then + if !(isMethodParam && sym.isTerm) + && !(sym.is(Method) && sym.owner.isClass) + then + report.error( + em"""@consume cannot be used here. Only memeber methods and their term parameters + |can have @consume annotations.""", + tree.srcPos) + else if annotCls == defn.UseAnnot then + if !isMethodParam then + report.error( + em"@use cannot be used here. Only method parameters can have @use annotations.", + tree.srcPos) case _ => - end checkProperUse + end checkProperUseOrConsume end setupTraverser // --------------- Adding capture set variables ---------------------------------- @@ -735,7 +811,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case RetainingType(parent, refs) => needsVariable(parent) && !refs.tpes.exists: - case ref: TermRef => ref.isRootCapability + case ref: TermRef => ref.isCap case _ => false case AnnotatedType(parent, _) => needsVariable(parent) @@ -772,7 +848,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: /** Add a capture set variable to `tp` if necessary. */ private def addVar(tp: Type, owner: Symbol)(using Context): Type = - decorate(tp, CaptureSet.Var(owner, _, level = currentLevel)) + decorate(tp, CaptureSet.Var(owner, _, level = ccState.currentLevel)) /** A map that adds capture sets at all contra- and invariant positions * in a type where a capture set would be needed. This is used to make types @@ -780,7 +856,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * We don't need to add in covariant positions since pure types are * anyway compatible with capturing types. */ - private def fluidify(using Context) = new TypeMap with IdempotentCaptRefMap: + private def fluidify(using Context) = new TypeMap: def apply(t: Type): Type = t match case t: MethodType => mapOver(t) @@ -798,37 +874,14 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if variance > 0 then t1 else decorate(t1, Function.const(CaptureSet.Fluid)) - /** Pull out an embedded capture set from a part of `tp` */ - def normalizeCaptures(tp: Type)(using Context): Type = tp match - case tp @ RefinedType(parent @ CapturingType(parent1, refs), rname, rinfo) => - CapturingType(tp.derivedRefinedType(parent1, rname, rinfo), refs, parent.isBoxed) - case tp: RecType => - tp.parent match - case parent @ CapturingType(parent1, refs) => - CapturingType(tp.derivedRecType(parent1), refs, parent.isBoxed) - case _ => - tp // can return `tp` here since unlike RefinedTypes, RecTypes are never created - // by `mapInferred`. Hence if the underlying type admits capture variables - // a variable was already added, and the first case above would apply. - case AndType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) => - assert(tp1.isBoxed == tp2.isBoxed) - CapturingType(AndType(parent1, parent2), refs1 ** refs2, tp1.isBoxed) - case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) => - assert(tp1.isBoxed == tp2.isBoxed) - CapturingType(OrType(parent1, parent2, tp.isSoft), refs1 ++ refs2, tp1.isBoxed) - case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2) => - CapturingType(OrType(parent1, tp2, tp.isSoft), refs1, tp1.isBoxed) - case tp @ OrType(tp1, tp2 @ CapturingType(parent2, refs2)) => - CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, tp2.isBoxed) - case tp @ AppliedType(tycon, args) - if !defn.isFunctionClass(tp.dealias.typeSymbol) && (tp.dealias eq tp) => - tp.derivedAppliedType(tycon, args.mapConserve(box)) - case tp: RealTypeBounds => - tp.derivedTypeBounds(tp.lo, box(tp.hi)) - case tp: LazyRef => - normalizeCaptures(tp.ref) - case _ => - tp + /** Replace all universal capture sets in this type by */ + private def makeUnchecked(using Context): TypeMap = new TypeMap with FollowAliasesMap: + def apply(t: Type) = t match + case t @ CapturingType(parent, refs) => + val parent1 = this(parent) + if refs.containsRootCapability then t.derivedCapturingType(parent1, CaptureSet.Fluid) + else t + case _ => mapFollowingAliases(t) /** Run setup on a compilation unit with given `tree`. * @param recheckDef the function to run for completing a val or def @@ -856,7 +909,12 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: var retained = ann.retainedElems.toArray for i <- 0 until retained.length do val refTree = retained(i) - for ref <- refTree.toCaptureRefs do + val refs = + try refTree.toCaptureRefs + catch case ex: IllegalCaptureRef => + report.error(em"Illegal capture reference: ${ex.getMessage.nn}", refTree.srcPos) + Nil + for ref <- refs do def pos = if refTree.span.exists then refTree.srcPos else if ann.span.exists then ann.srcPos @@ -877,6 +935,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: for j <- 0 until retained.length if j != i r <- retained(j).toCaptureRefs + if !r.isRootCapability yield r val remaining = CaptureSet(others*) check(remaining, remaining) diff --git a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala index 1372ebafe82f..cdc3afff8a2d 100644 --- a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala +++ b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala @@ -116,7 +116,7 @@ object Synthetics: def transformUnapplyCaptures(info: Type)(using Context): Type = info match case info: MethodType => val paramInfo :: Nil = info.paramInfos: @unchecked - val newParamInfo = CapturingType(paramInfo, CaptureSet.universal) + val newParamInfo = CapturingType(paramInfo, CaptureSet.fresh()) val trackedParam = info.paramRefs.head def newResult(tp: Type): Type = tp match case tp: MethodOrPoly => @@ -132,23 +132,26 @@ object Synthetics: val (pt: PolyType) = info: @unchecked val (mt: MethodType) = pt.resType: @unchecked val (enclThis: ThisType) = owner.thisType: @unchecked + val paramCaptures = CaptureSet(enclThis, root.cap) pt.derivedLambdaType(resType = MethodType(mt.paramNames)( - mt1 => mt.paramInfos.map(_.capturing(CaptureSet.universal)), + mt1 => mt.paramInfos.map(_.capturing(paramCaptures)), mt1 => CapturingType(mt.resType, CaptureSet(enclThis, mt1.paramRefs.head)))) def transformCurriedTupledCaptures(info: Type, owner: Symbol) = val (et: ExprType) = info: @unchecked val (enclThis: ThisType) = owner.thisType: @unchecked - def mapFinalResult(tp: Type, f: Type => Type): Type = - val defn.FunctionNOf(args, res, isContextual) = tp: @unchecked - if defn.isFunctionNType(res) then - defn.FunctionNOf(args, mapFinalResult(res, f), isContextual) - else + def mapFinalResult(tp: Type, f: Type => Type): Type = tp match + case FunctionOrMethod(args, res) => + tp.derivedFunctionOrMethod(args, mapFinalResult(res, f)) + case _ => f(tp) ExprType(mapFinalResult(et.resType, CapturingType(_, CaptureSet(enclThis)))) def transformCompareCaptures = - MethodType(defn.ObjectType.capturing(CaptureSet.universal) :: Nil, defn.BooleanType) + val (enclThis: ThisType) = symd.owner.thisType: @unchecked + MethodType( + defn.ObjectType.capturing(CaptureSet(root.cap, enclThis)) :: Nil, + defn.BooleanType) symd.copySymDenotation(info = symd.name match case DefaultGetterName(nme.copy, n) => diff --git a/compiler/src/dotty/tools/dotc/cc/ccConfig.scala b/compiler/src/dotty/tools/dotc/cc/ccConfig.scala new file mode 100644 index 000000000000..4c06f4a0843d --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/ccConfig.scala @@ -0,0 +1,57 @@ +package dotty.tools +package dotc +package cc + +import core.Contexts.Context +import config.{Feature, SourceVersion} + +object ccConfig: + + /** If enabled, use a special path in recheckClosure for closures + * to compare the result tpt of the anonymous functon with the expected + * result type. This can narrow the scope of error messages. + */ + inline val preTypeClosureResults = false + + /** If this and `preTypeClosureResults` are both enabled, disable `preTypeClosureResults` + * for eta expansions. This can improve some error messages. + */ + inline val handleEtaExpansionsSpecially = true + + /** Don't require @use for reach capabilities that are accessed + * only in a nested closure. This is unsound without additional + * mitigation measures, as shown by unsound-reach-5.scala. + */ + inline val deferredReaches = false + + /** Check that if a type map (which is not a BiTypeMap) maps initial capture + * set variable elements to themselves it will not map any elements added in + * the future to something else. That is, we can safely use a capture set + * variable itself as the image under the map. By default this is off since it + * is a bit expensive to check. + */ + inline val checkSkippedMaps = false + + /** Always repeat a capture checking run at least once if there are no errors + * yet. Used for stress-testing the logic for when a new capture checking run needs + * to be scheduled because a provisionally solved capture set was later extended. + * So far this happens only in very few tests. With the flag on, the logic is + * tested for all tests except neg tests. + */ + inline val alwaysRepeatRun = false + + /** After capture checking, check that no capture set contains ParamRefs that are outside + * its scope. This used to occur and was fixed by healTypeParam. It should no longer + * occur now. + */ + inline val postCheckCapturesets = false + + /** If true, turn on separation checking */ + def useSepChecks(using Context): Boolean = + Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.7`) + + /** Not used currently. Handy for trying out new features */ + def newScheme(using Context): Boolean = + Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.8`) + +end ccConfig \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/cc/root.scala b/compiler/src/dotty/tools/dotc/cc/root.scala new file mode 100644 index 000000000000..ee668efa8378 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/root.scala @@ -0,0 +1,355 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Types.*, Symbols.*, Contexts.*, Annotations.*, Flags.* +import StdNames.nme +import ast.tpd.* +import Decorators.* +import typer.ErrorReporting.errorType +import Names.TermName +import NameKinds.ExistentialBinderName +import NameOps.isImpureFunction +import reporting.Message +import util.{SimpleIdentitySet, EqHashMap} +import util.Spans.NoSpan +import annotation.constructorOnly + +/** A module defining three kinds of root capabilities + * - `cap` of kind `Global`: This is the global root capability. Among others it is + * used in the types of formal parameters, in type bounds, and in self types. + * `cap` does not subsume other capabilities, except in arguments of + * `withCapAsRoot` calls. + * - Instances of Fresh(hidden), of kind Fresh. These do subsume other capabilties in scope. + * They track with hidden sets which other capabilities were subsumed. + * Hidden sets are inspected by separation checking. + * - Instances of Result(binder), of kind Result. These are existentials associated with + * the result types of dependent methods. They don't subsume other capabilties. + * + * Representation: + * + * - `cap` is just the TermRef `scala.caps.cap` defined in the `caps` module + * - `Fresh` and `Result` instances are annotated types of `scala.caps.cap` + * with a special `root.Annot` annotation. The symbol of the annotation is + * `annotation.internal.rootCapability`. The annotation carries a kind, which provides + * a hidden set for Fresh instances and a binder method type for Result instances. + * + * Setup: + * + * In the setup phase, `cap` instances in the result of a dependent function type + * or method type such as `(x: T): C^{cap}` are converted to `Result(binder)` instances, + * where `binder` refers to the method type. Most other cap instances are mapped to + * Fresh instances instead. For example the `cap` in the result of `T => C^{cap}` + * is mapped to a Fresh instance. + * + * If one needs to use a dependent function type yet one still want to map `cap` to + * a fresh instance instead an existential root, one can achieve that by the use + * of a type alias. For instance, the following type creates an existential for `^`: + * + * (x: A) => (C^{x}, D^) + * + * By contrast, this variant creates a fresh instance instead: + * + * type F[X] = (x: A) => (C^{x}, X) + * F[D^] + * + * The trick is that the argument D^ is mapped to D^{fresh} before the `F` alias + * is expanded. + */ +object root: + + enum Kind: + case Result(binder: MethodType) + case Fresh(hidden: CaptureSet.HiddenSet) + case Global + + override def equals(other: Any): Boolean = + (this eq other.asInstanceOf[AnyRef]) || this.match + case Kind.Result(b1) => other match + case Kind.Result(b2) => b1 eq b2 + case _ => false + case Kind.Fresh(h1) => other match + case Kind.Fresh(h2) => h1 eq h2 + case _ => false + case Kind.Global => false + end Kind + + /** The annotation of a root instance */ + case class Annot(kind: Kind)(using @constructorOnly ictx: Context) extends Annotation: + + /** id printed under -uniqid, for debugging */ + val id = + val ccs = ccState + ccs.rootId += 1 + ccs.rootId + + override def symbol(using Context) = defn.RootCapabilityAnnot + override def tree(using Context) = New(symbol.typeRef, Nil) + override def derivedAnnotation(tree: Tree)(using Context): Annotation = this + + private var myOriginalKind = kind + def originalBinder: MethodType = myOriginalKind.asInstanceOf[Kind.Result].binder + + def derivedAnnotation(binder: MethodType)(using Context): Annotation = kind match + case Kind.Result(b) if b ne binder => + val ann = Annot(Kind.Result(binder)) + ann.myOriginalKind = myOriginalKind + ann + case _ => + this + + override def hash: Int = kind.hashCode + override def eql(that: Annotation) = that match + case Annot(kind) => this.kind eq kind + case _ => false + + /** Special treatment of `SubstBindingMaps` which can change the binder of a + * Result instances + */ + override def mapWith(tm: TypeMap)(using Context) = kind match + case Kind.Result(binder) => tm match + case tm: Substituters.SubstBindingMap[MethodType] @unchecked if tm.from eq binder => + derivedAnnotation(tm.to) + case tm: Substituters.SubstBindingsMap => + var i = 0 + while i < tm.from.length && (tm.from(i) ne binder) do i += 1 + if i < tm.from.length then derivedAnnotation(tm.to(i).asInstanceOf[MethodType]) + else this + case _ => this + case _ => this + end Annot + + def cap(using Context): TermRef = defn.captureRoot.termRef + + /** The type of fresh references */ + type Fresh = AnnotatedType + + object Fresh: + /** Constructor and extractor methods for "fresh" capabilities */ + private def make(owner: Symbol)(using Context): CaptureRef = + if ccConfig.useSepChecks then + val hiddenSet = CaptureSet.HiddenSet(owner) + val res = AnnotatedType(cap, Annot(Kind.Fresh(hiddenSet))) + hiddenSet.owningCap = res + //assert(hiddenSet.id != 3) + res + else + cap + + def withOwner(owner: Symbol)(using Context): CaptureRef = make(owner) + def apply()(using Context): CaptureRef = make(NoSymbol) + + def unapply(tp: AnnotatedType): Option[CaptureSet.HiddenSet] = tp.annot match + case Annot(Kind.Fresh(hidden)) => Some(hidden) + case _ => None + end Fresh + + /** The type of existentially bound references */ + type Result = AnnotatedType + + object Result: + def apply(binder: MethodType)(using Context): Result = + val hiddenSet = CaptureSet.HiddenSet(NoSymbol) + val res = AnnotatedType(cap, Annot(Kind.Result(binder))) + hiddenSet.owningCap = res + res + + def unapply(tp: Result)(using Context): Option[MethodType] = tp.annot match + case Annot(Kind.Result(binder)) => Some(binder) + case _ => None + end Result + + def unapply(root: CaptureRef)(using Context): Option[Kind] = root match + case root @ AnnotatedType(_, ann: Annot) => Some(ann.kind) + case _ if root.isCap => Some(Kind.Global) + case _ => None + + /** Map each occurrence of cap to a different Fresh instance + * Exception: CapSet^ stays as it is. + */ + class CapToFresh(owner: Symbol)(using Context) extends BiTypeMap, FollowAliasesMap: + thisMap => + + override def apply(t: Type) = + if variance <= 0 then t + else t match + case t: CaptureRef if t.isCap => + Fresh.withOwner(owner) + case t @ CapturingType(parent: TypeRef, _) if parent.symbol == defn.Caps_CapSet => + t + case t @ CapturingType(_, _) => + mapOver(t) + case t @ AnnotatedType(parent, ann) => + val parent1 = this(parent) + if ann.symbol.isRetains && ann.tree.toCaptureSet.containsCap then + this(CapturingType(parent1, ann.tree.toCaptureSet)) + else + t.derivedAnnotatedType(parent1, ann) + case _ => + mapFollowingAliases(t) + + override def fuse(next: BiTypeMap)(using Context) = next match + case next: Inverse => assert(false); Some(IdentityTypeMap) + case _ => None + + override def toString = "CapToFresh" + + class Inverse extends BiTypeMap, FollowAliasesMap: + def apply(t: Type): Type = t match + case t @ Fresh(_) => cap + case t @ CapturingType(_, refs) => mapOver(t) + case _ => mapFollowingAliases(t) + + override def fuse(next: BiTypeMap)(using Context) = next match + case next: CapToFresh => assert(false); Some(IdentityTypeMap) + case _ => None + + def inverse = thisMap + override def toString = thisMap.toString + ".inverse" + + lazy val inverse = Inverse() + + end CapToFresh + + /** Maps cap to fresh */ + def capToFresh(tp: Type, owner: Symbol = NoSymbol)(using Context): Type = + if ccConfig.useSepChecks then CapToFresh(owner)(tp) else tp + + /** Maps fresh to cap */ + def freshToCap(tp: Type)(using Context): Type = + if ccConfig.useSepChecks then CapToFresh(NoSymbol).inverse(tp) else tp + + /** Map top-level free existential variables one-to-one to Fresh instances */ + def resultToFresh(tp: Type)(using Context): Type = + val subst = new TypeMap: + val seen = EqHashMap[Annotation, CaptureRef]() + var localBinders: SimpleIdentitySet[MethodType] = SimpleIdentitySet.empty + + def apply(t: Type): Type = t match + case t @ Result(binder) => + if localBinders.contains(binder) then t // keep bound references + else seen.getOrElseUpdate(t.annot, Fresh()) // map free references to Fresh() + case t: MethodType => + // skip parameters + val saved = localBinders + if t.marksExistentialScope then localBinders = localBinders + t + try t.derivedLambdaType(resType = this(t.resType)) + finally localBinders = saved + case t: PolyType => + // skip parameters + t.derivedLambdaType(resType = this(t.resType)) + case _ => + mapOver(t) + + subst(tp) + end resultToFresh + + /** Replace all occurrences of `cap` (or fresh) in parts of this type by an existentially bound + * variable bound by `mt`. + * Stop at function or method types since these have been mapped before. + */ + def toResult(tp: Type, mt: MethodType, fail: Message => Unit)(using Context): Type = + + abstract class CapMap extends BiTypeMap: + override def mapOver(t: Type): Type = t match + case t @ FunctionOrMethod(args, res) if variance > 0 && !t.isAliasFun => + t // `t` should be mapped in this case by a different call to `mapCap`. + case t: (LazyRef | TypeVar) => + mapConserveSuper(t) + case _ => + super.mapOver(t) + + object toVar extends CapMap: + private val seen = EqHashMap[CaptureRef, Result]() + + def apply(t: Type) = t match + case t: CaptureRef if t.isCapOrFresh => + if variance > 0 then + seen.getOrElseUpdate(t, Result(mt)) + else + if variance == 0 then + fail(em"""$tp captures the root capability `cap` in invariant position. + |This capability cannot be converted to an existential in the result type of a function.""") + // we accept variance < 0, and leave the cap as it is + super.mapOver(t) + case defn.FunctionNOf(args, res, contextual) if t.typeSymbol.name.isImpureFunction => + if variance > 0 then + super.mapOver: + defn.FunctionNOf(args, res, contextual) + .capturing(Result(mt).singletonCaptureSet) + else mapOver(t) + case _ => + mapOver(t) + //.showing(i"mapcap $t = $result") + override def toString = "toVar" + + object inverse extends BiTypeMap: + def apply(t: Type) = t match + case t @ Result(`mt`) => + // do a reverse getOrElseUpdate on `seen` to produce the + // `Fresh` assosicated with `t` + val it = seen.iterator + var ref: CaptureRef | Null = null + while it.hasNext && ref == null do + val (k, v) = it.next + if v.annot eq t.annot then ref = k + if ref == null then + ref = Fresh() + seen(ref) = t + ref + case _ => mapOver(t) + def inverse = toVar.this + override def toString = "toVar.inverse" + end toVar + + toVar(tp) + end toResult + + /** Map global roots in function results to result roots */ + def toResultInResults(fail: Message => Unit, keepAliases: Boolean = false)(using Context): TypeMap = new TypeMap with FollowAliasesMap: + def apply(t: Type): Type = t match + case defn.RefinedFunctionOf(mt) => + val mt1 = apply(mt) + if mt1 ne mt then mt1.toFunctionType(alwaysDependent = true) + else t + case t: MethodType if variance > 0 && t.marksExistentialScope => + val t1 = mapOver(t).asInstanceOf[MethodType] + t1.derivedLambdaType(resType = toResult(t1.resType, t1, fail)) + case CapturingType(parent, refs) => + t.derivedCapturingType(this(parent), refs) + case t: (LazyRef | TypeVar) => + mapConserveSuper(t) + case _ => + try + if keepAliases then mapOver(t) + else mapFollowingAliases(t) + catch case ex: AssertionError => + println(i"error while mapping $t") + throw ex + end toResultInResults + + /** If `refs` contains an occurrence of `cap` or `cap.rd`, the current context + * with an added property PrintFresh. This addition causes all occurrences of + * `Fresh` to be printed as `fresh` instead of `cap`, so that one avoids + * confusion in error messages. + */ + def printContext(refs: (Type | CaptureSet)*)(using Context): Context = + def hasCap = new TypeAccumulator[Boolean]: + def apply(x: Boolean, t: Type) = + x || t.dealiasKeepAnnots.match + case Fresh(_) => false + case t: TermRef => t.isCap || this(x, t.widen) + case x: ThisType => false + case _ => foldOver(x, t) + + def containsCap(x: Type | CaptureSet): Boolean = x match + case tp: Type => + hasCap(false, tp) + case refs: CaptureSet => + refs.elems.exists(_.stripReadOnly.isCap) + + if refs.exists(containsCap) then ctx.withProperty(PrintFresh, Some(())) + else ctx + end printContext +end root \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 6df190f3147e..8fda99be6896 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -61,7 +61,8 @@ object Feature: (pureFunctions, "Enable pure functions for capture checking"), (captureChecking, "Enable experimental capture checking"), (into, "Allow into modifier on parameter types"), - (modularity, "Enable experimental modularity features") + (modularity, "Enable experimental modularity features"), + (packageObjectValues, "Enable experimental package objects as values"), ) // legacy language features from Scala 2 that are no longer supported. @@ -153,6 +154,10 @@ object Feature: case Some(v) => v case none => sourceVersionSetting + /* Should we behave as scala 2?*/ + def shouldBehaveAsScala2(using Context): Boolean = + ctx.settings.YcompileScala2Library.value || sourceVersion.isScala2 + def migrateTo3(using Context): Boolean = sourceVersion == `3.0-migration` diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 4ee4d17cd63e..0517d5b46b9e 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -167,6 +167,7 @@ private sealed trait WarningSettings: private val WenumCommentDiscard = BooleanSetting(WarningSetting, "Wenum-comment-discard", "Warn when a comment ambiguously assigned to multiple enum cases is discarded.") private val WimplausiblePatterns = BooleanSetting(WarningSetting, "Wimplausible-patterns", "Warn if comparison with a pattern value looks like it might always fail.") private val WunstableInlineAccessors = BooleanSetting(WarningSetting, "WunstableInlineAccessors", "Warn an inline methods has references to non-stable binary APIs.") + private val WtoStringInterpolated = BooleanSetting(WarningSetting, "Wtostring-interpolated", "Warn a standard interpolator used toString on a reference type.") private val Wunused: Setting[List[ChoiceWithHelp[String]]] = MultiChoiceHelpSetting( WarningSetting, name = "Wunused", @@ -308,6 +309,7 @@ private sealed trait WarningSettings: def enumCommentDiscard(using Context): Boolean = allOr(WenumCommentDiscard) def implausiblePatterns(using Context): Boolean = allOr(WimplausiblePatterns) def unstableInlineAccessors(using Context): Boolean = allOr(WunstableInlineAccessors) + def toStringInterpolated(using Context): Boolean = allOr(WtoStringInterpolated) def checkInit(using Context): Boolean = allOr(WcheckInit) /** -X "Extended" or "Advanced" settings */ @@ -453,6 +455,7 @@ private sealed trait YSettings: val YccDebug: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-debug", "Used in conjunction with captureChecking language import, debug info for captured references.") val YccNew: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-new", "Used in conjunction with captureChecking language import, try out new variants (debug option)") val YccLog: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-log", "Used in conjunction with captureChecking language import, print tracing and debug info") + val YccVerbose: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-verbose", "Print root capabilities with more details") val YccPrintSetup: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-print-setup", "Used in conjunction with captureChecking language import, print trees after cc.Setup phase") /** Area-specific debug output */ diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettingsProperties.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettingsProperties.scala index e42d2d53529e..12de678d6df2 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettingsProperties.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettingsProperties.scala @@ -25,7 +25,8 @@ object ScalaSettingsProperties: ScalaRelease.values.toList.map(_.show) def supportedSourceVersions: List[String] = - (SourceVersion.values.toList.diff(SourceVersion.illegalSourceVersionNames)).toList.map(_.toString) + SourceVersion.values.diff(SourceVersion.illegalInSettings) + .map(_.toString).toList def supportedLanguageFeatures: List[ChoiceWithHelp[String]] = Feature.values.map((n, d) => ChoiceWithHelp(n.toString, d)) diff --git a/compiler/src/dotty/tools/dotc/config/SourceVersion.scala b/compiler/src/dotty/tools/dotc/config/SourceVersion.scala index 30a88fb79f2a..d662d3c0d412 100644 --- a/compiler/src/dotty/tools/dotc/config/SourceVersion.scala +++ b/compiler/src/dotty/tools/dotc/config/SourceVersion.scala @@ -8,7 +8,8 @@ import Feature.isPreviewEnabled import util.Property enum SourceVersion: - case `3.0-migration`, `3.0`, `3.1` // Note: do not add `3.1-migration` here, 3.1 is the same language as 3.0. + case `3.0-migration`, `3.0` + case `3.1-migration`, `3.1` case `3.2-migration`, `3.2` case `3.3-migration`, `3.3` case `3.4-migration`, `3.4` @@ -16,7 +17,9 @@ enum SourceVersion: case `3.6-migration`, `3.6` case `3.7-migration`, `3.7` case `3.8-migration`, `3.8` + // Add 3.x-migration and 3.x here // !!! Keep in sync with scala.runtime.stdlibPatches.language !!! + case `2.13` case `future-migration`, `future` case `never` // needed for MigrationVersion.errorFrom if we never want to issue an error @@ -33,6 +36,8 @@ enum SourceVersion: def isAtMost(v: SourceVersion) = stable.ordinal <= v.ordinal + def isScala2 = this == `2.13` + def enablesFewerBraces = isAtLeast(`3.3`) def enablesClauseInterleaving = isAtLeast(`3.6`) def enablesNewGivens = isAtLeast(`3.6`) @@ -40,10 +45,18 @@ enum SourceVersion: def enablesBetterFors(using Context) = isAtLeast(`3.7`) && isPreviewEnabled object SourceVersion extends Property.Key[SourceVersion]: - def defaultSourceVersion = `3.7` + + /* The default source version used by the built compiler */ + val defaultSourceVersion = `3.7` + + /* Illegal source versions that may not appear in the settings `-source:<...>` */ + val illegalInSettings = List(`2.13`, `3.1-migration`, `never`) + + /* Illegal source versions that may not appear as an import `import scala.language.<...>` */ + val illegalInImports = List(`3.1-migration`, `never`) /** language versions that may appear in a language import, are deprecated, but not removed from the standard library. */ - val illegalSourceVersionNames = List("3.1-migration", "never").map(_.toTermName) + val illegalSourceVersionNames = illegalInImports.map(_.toString.toTermName) /** language versions that the compiler recognises. */ val validSourceVersionNames = values.toList.map(_.toString.toTermName) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index e44bfcee2cf7..22632065674c 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -15,7 +15,7 @@ import Comments.{Comment, docCtx} import util.Spans.NoSpan import config.Feature import Symbols.requiredModuleRef -import cc.{CaptureSet, RetainingType, Existential} +import cc.{CaptureSet, RetainingType, readOnly} import ast.tpd.ref import scala.annotation.tailrec @@ -505,6 +505,9 @@ class Definitions { @tu lazy val ScalaRuntime_toArray: Symbol = ScalaRuntimeModule.requiredMethod(nme.toArray) @tu lazy val ScalaRuntime_toObjectArray: Symbol = ScalaRuntimeModule.requiredMethod(nme.toObjectArray) + @tu lazy val MurmurHash3Module: Symbol = requiredModule("scala.util.hashing.MurmurHash3") + @tu lazy val MurmurHash3_productHash = MurmurHash3Module.info.member(termName("productHash")).suchThat(_.info.firstParamTypes.size == 3).symbol + @tu lazy val BoxesRunTimeModule: Symbol = requiredModule("scala.runtime.BoxesRunTime") @tu lazy val BoxesRunTimeModule_externalEquals: Symbol = BoxesRunTimeModule.info.decl(nme.equals_).suchThat(toDenot(_).info.firstParamTypes.size == 2).symbol @tu lazy val ScalaStaticsModule: Symbol = requiredModule("scala.runtime.Statics") @@ -995,20 +998,23 @@ class Definitions { @tu lazy val CapsModule: Symbol = requiredPackage("scala.caps") @tu lazy val captureRoot: TermSymbol = CapsModule.requiredValue("cap") - @tu lazy val Caps_Capability: TypeSymbol = CapsModule.requiredType("Capability") + @tu lazy val Caps_Capability: ClassSymbol = requiredClass("scala.caps.Capability") @tu lazy val Caps_CapSet: ClassSymbol = requiredClass("scala.caps.CapSet") @tu lazy val CapsInternalModule: Symbol = requiredModule("scala.caps.internal") @tu lazy val Caps_reachCapability: TermSymbol = CapsInternalModule.requiredMethod("reachCapability") + @tu lazy val Caps_readOnlyCapability: TermSymbol = CapsInternalModule.requiredMethod("readOnlyCapability") @tu lazy val Caps_capsOf: TermSymbol = CapsInternalModule.requiredMethod("capsOf") - @tu lazy val Caps_Exists: ClassSymbol = requiredClass("scala.caps.Exists") @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_ContainsTrait: TypeSymbol = CapsModule.requiredType("Contains") @tu lazy val Caps_ContainsModule: Symbol = requiredModule("scala.caps.Contains") @tu lazy val Caps_containsImpl: TermSymbol = Caps_ContainsModule.requiredMethod("containsImpl") + @tu lazy val Caps_Mutable: ClassSymbol = requiredClass("scala.caps.Mutable") + @tu lazy val Caps_SharedCapability: ClassSymbol = requiredClass("scala.caps.SharedCapability") /** The same as CaptureSet.universal but generated implicitly for references of Capability subtypes */ - @tu lazy val universalCSImpliedByCapability = CaptureSet(captureRoot.termRef) + @tu lazy val universalCSImpliedByCapability = CaptureSet(captureRoot.termRef.readOnly) @tu lazy val PureClass: Symbol = requiredClass("scala.Pure") @@ -1067,6 +1073,8 @@ class Definitions { @tu lazy val UncheckedCapturesAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedCaptures") @tu lazy val UntrackedCapturesAnnot: ClassSymbol = requiredClass("scala.caps.unsafe.untrackedCaptures") @tu lazy val UseAnnot: ClassSymbol = requiredClass("scala.caps.use") + @tu lazy val ConsumeAnnot: ClassSymbol = requiredClass("scala.caps.consume") + @tu lazy val RefineOverrideAnnot: ClassSymbol = requiredClass("scala.caps.internal.refineOverride") @tu lazy val VolatileAnnot: ClassSymbol = requiredClass("scala.volatile") @tu lazy val LanguageFeatureMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.languageFeature") @tu lazy val BeanGetterMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.beanGetter") @@ -1082,6 +1090,8 @@ class Definitions { @tu lazy val TargetNameAnnot: ClassSymbol = requiredClass("scala.annotation.targetName") @tu lazy val VarargsAnnot: ClassSymbol = requiredClass("scala.annotation.varargs") @tu lazy val ReachCapabilityAnnot = requiredClass("scala.annotation.internal.reachCapability") + @tu lazy val RootCapabilityAnnot = requiredClass("scala.caps.internal.rootCapability") + @tu lazy val ReadOnlyCapabilityAnnot = requiredClass("scala.annotation.internal.readOnlyCapability") @tu lazy val RequiresCapabilityAnnot: ClassSymbol = requiredClass("scala.annotation.internal.requiresCapability") @tu lazy val RetainsAnnot: ClassSymbol = requiredClass("scala.annotation.retains") @tu lazy val RetainsCapAnnot: ClassSymbol = requiredClass("scala.annotation.retainsCap") @@ -1105,6 +1115,10 @@ class Definitions { @tu lazy val MetaAnnots: Set[Symbol] = NonBeanMetaAnnots + BeanGetterMetaAnnot + BeanSetterMetaAnnot + // Set of annotations that are not printed in types except under -Yprint-debug + @tu lazy val SilentAnnots: Set[Symbol] = + Set(InlineParamAnnot, ErasedParamAnnot, RefineOverrideAnnot) + // A list of annotations that are commonly used to indicate that a field/method argument or return // type is not null. These annotations are used by the nullification logic in JavaNullInterop to // improve the precision of type nullification. @@ -1211,13 +1225,8 @@ class Definitions { */ def unapply(tpe: RefinedType)(using Context): Option[MethodOrPoly] = tpe.refinedInfo match - case mt: MethodType - if tpe.refinedName == nme.apply - && isFunctionType(tpe.parent) - && !Existential.isExistentialMethod(mt) => Some(mt) - case mt: PolyType - if tpe.refinedName == nme.apply - && isFunctionType(tpe.parent) => Some(mt) + case mt: MethodOrPoly + if tpe.refinedName == nme.apply && isFunctionType(tpe.parent) => Some(mt) case _ => None end RefinedFunctionOf @@ -1528,9 +1537,7 @@ class Definitions { denot.sourceModule.info = denot.typeRef // we run into a cyclic reference when patching if this line is omitted patch2(denot, patchCls) - if ctx.settings.YcompileScala2Library.value then - () - else if denot.name == tpnme.Predef.moduleClassName && denot.symbol == ScalaPredefModuleClass then + if denot.name == tpnme.Predef.moduleClassName && denot.symbol == ScalaPredefModuleClass then patchWith(ScalaPredefModuleClassPatch) else if denot.name == tpnme.language.moduleClassName && denot.symbol == LanguageModuleClass then patchWith(LanguageModuleClassPatch) @@ -1552,6 +1559,9 @@ class Definitions { @tu lazy val pureSimpleClasses = Set(StringClass, NothingClass, NullClass) ++ ScalaValueClasses() + @tu lazy val capabilityWrapperAnnots: Set[Symbol] = + Set(ReachCapabilityAnnot, ReadOnlyCapabilityAnnot, MaybeCapabilityAnnot, RootCapabilityAnnot) + @tu lazy val AbstractFunctionType: Array[TypeRef] = mkArityArray("scala.runtime.AbstractFunction", MaxImplementedFunctionArity, 0).asInstanceOf[Array[TypeRef]] val AbstractFunctionClassPerRun: PerRun[Array[Symbol]] = new PerRun(AbstractFunctionType.map(_.symbol.asClass)) def AbstractFunctionClass(n: Int)(using Context): Symbol = AbstractFunctionClassPerRun()(using ctx)(n) @@ -1868,7 +1878,7 @@ class Definitions { || tp.derivesFrom(defn.PolyFunctionClass) // TODO check for refinement? private def withSpecMethods(cls: ClassSymbol, bases: List[Name], paramTypes: Set[TypeRef]) = - if !ctx.settings.YcompileScala2Library.value then + if !Feature.shouldBehaveAsScala2 then for base <- bases; tp <- paramTypes do cls.enter(newSymbol(cls, base.specializedName(List(tp)), Method, ExprType(tp))) cls @@ -1911,7 +1921,7 @@ class Definitions { case List(x, y) => Tuple2SpecializedParamClasses().contains(x.classSymbol) && Tuple2SpecializedParamClasses().contains(y.classSymbol) case _ => false && base.owner.denot.info.member(base.name.specializedName(args)).exists // when dotc compiles the stdlib there are no specialised classes - && !ctx.settings.YcompileScala2Library.value // We do not add the specilized TupleN methods/classes when compiling the stdlib + && !Feature.shouldBehaveAsScala2 // We do not add the specilized TupleN methods/classes when compiling the stdlib def isSpecializableFunction(cls: ClassSymbol, paramTypes: List[Type], retType: Type)(using Context): Boolean = paramTypes.length <= 2 @@ -1933,7 +1943,7 @@ class Definitions { case _ => false }) - && !ctx.settings.YcompileScala2Library.value // We do not add the specilized FunctionN methods/classes when compiling the stdlib + && !Feature.shouldBehaveAsScala2 // We do not add the specilized FunctionN methods/classes when compiling the stdlib @tu lazy val Function0SpecializedApplyNames: List[TermName] = for r <- Function0SpecializedReturnTypes @@ -2092,7 +2102,7 @@ class Definitions { Caps_Capability, // TODO: Remove when Capability is stabilized RequiresCapabilityAnnot, captureRoot, Caps_CapSet, Caps_ContainsTrait, Caps_ContainsModule, Caps_ContainsModule.moduleClass, UseAnnot, - Caps_Exists, + Caps_Mutable, Caps_SharedCapability, ConsumeAnnot, CapsUnsafeModule, CapsUnsafeModule.moduleClass, CapsInternalModule, CapsInternalModule.moduleClass, RetainsAnnot, RetainsCapAnnot, RetainsByNameAnnot) diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index 0775b3caaf0c..57bf870c6b64 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -597,7 +597,6 @@ object Flags { val JavaInterface: FlagSet = JavaDefined | NoInits | Trait val JavaProtected: FlagSet = JavaDefined | Protected val MethodOrLazy: FlagSet = Lazy | Method - val MutableOrLazy: FlagSet = Lazy | Mutable val MethodOrLazyOrMutable: FlagSet = Lazy | Method | Mutable val LiftedMethod: FlagSet = Lifted | Method val LocalParam: FlagSet = Local | Param diff --git a/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala b/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala index 5a8938602523..75c23bb003b5 100644 --- a/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala @@ -33,6 +33,18 @@ class GadtConstraint private ( reverseMapping = reverseMapping.updated(tv.origin, sym), ) + def replace(param: TypeParamRef, tp: Type)(using Context) = + var constr = constraint + for + poly <- constraint.domainLambdas + paramRef <- poly.paramRefs + do + val entry0 = constr.entry(paramRef) + val entry1 = entry0.substParam(param, tp) + if entry1 ne entry0 then + constr = constr.updateEntry(paramRef, entry1) + withConstraint(constr) + /** Is `sym1` ordered to be less than `sym2`? */ def isLess(sym1: Symbol, sym2: Symbol)(using Context): Boolean = constraint.isLess(tvarOrError(sym1).origin, tvarOrError(sym2).origin) @@ -245,6 +257,9 @@ sealed trait GadtState { result } + def replace(param: TypeParamRef, tp: Type)(using Context) = + gadt = gadt.replace(param, tp) + /** See [[ConstraintHandling.approximation]] */ def approximation(sym: Symbol, fromBelow: Boolean, maxLevel: Int = Int.MaxValue)(using Context): Type = { approximation(gadt.tvarOrError(sym).origin, fromBelow, maxLevel).match diff --git a/compiler/src/dotty/tools/dotc/core/NameOps.scala b/compiler/src/dotty/tools/dotc/core/NameOps.scala index 415aa049c587..fe3ac68f08c5 100644 --- a/compiler/src/dotty/tools/dotc/core/NameOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NameOps.scala @@ -7,7 +7,7 @@ import java.nio.CharBuffer import scala.io.Codec import Int.MaxValue import Names.*, StdNames.*, Contexts.*, Symbols.*, Flags.*, NameKinds.*, Types.* -import util.Chars.{isOperatorPart, digit2int} +import util.Chars.{isOperatorPart, isIdentifierPart, digit2int} import Decorators.* import Definitions.* import nme.* @@ -78,9 +78,22 @@ object NameOps { def isUnapplyName: Boolean = name == nme.unapply || name == nme.unapplySeq def isRightAssocOperatorName: Boolean = name.lastPart.last == ':' - def isOperatorName: Boolean = name match - case name: SimpleName => name.exists(isOperatorPart) - case _ => false + /** Does this name match `[{letter | digit} '_'] op`? + * + * See examples in [[NameOpsTest]]. + */ + def isOperatorName: Boolean = + name match + case name: SimpleName => + var i = name.length - 1 + // Ends with operator characters + while i >= 0 && isOperatorPart(name(i)) do i -= 1 + if i == -1 then return true + // Optionnally prefixed with alpha-numeric characters followed by `_` + if name(i) != '_' then return false + while i >= 0 && isIdentifierPart(name(i)) do i -= 1 + i == -1 + case _ => false /** Is name of a variable pattern? */ def isVarPattern: Boolean = diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 90e5544f19af..c33c795571e6 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -121,6 +121,7 @@ object StdNames { val BITMAP_CHECKINIT: N = s"${BITMAP_PREFIX}init$$" // initialization bitmap for checkinit values val BITMAP_CHECKINIT_TRANSIENT: N = s"${BITMAP_PREFIX}inittrans$$" // initialization bitmap for transient checkinit values val CC_REACH: N = "$reach" + val CC_READONLY: N = "$readOnly" val DEFAULT_GETTER: N = str.DEFAULT_GETTER val DEFAULT_GETTER_INIT: N = "$lessinit$greater" val DO_WHILE_PREFIX: N = "doWhile$" @@ -554,6 +555,7 @@ object StdNames { val materializeTypeTag: N = "materializeTypeTag" val mirror : N = "mirror" val moduleClass : N = "moduleClass" + val mut: N = "mut" val name: N = "name" val nameDollar: N = "$name" val ne: N = "ne" @@ -588,6 +590,7 @@ object StdNames { val productPrefix: N = "productPrefix" val quotes : N = "quotes" val raw_ : N = "raw" + val rd: N = "rd" val refl: N = "refl" val reflect: N = "reflect" val reflectiveSelectable: N = "reflectiveSelectable" diff --git a/compiler/src/dotty/tools/dotc/core/Substituters.scala b/compiler/src/dotty/tools/dotc/core/Substituters.scala index 96da91293d91..6cd238bb0e19 100644 --- a/compiler/src/dotty/tools/dotc/core/Substituters.scala +++ b/compiler/src/dotty/tools/dotc/core/Substituters.scala @@ -2,14 +2,13 @@ package dotty.tools.dotc package core import Types.*, Symbols.*, Contexts.* -import cc.CaptureSet.IdempotentCaptRefMap /** Substitution operations on types. See the corresponding `subst` and * `substThis` methods on class Type for an explanation. */ object Substituters: - final def subst(tp: Type, from: BindingType, to: BindingType, theMap: SubstBindingMap | Null)(using Context): Type = + final def subst[BT <: BindingType](tp: Type, from: BT, to: BT, theMap: SubstBindingMap[BT] | Null)(using Context): Type = tp match { case tp: BoundType => if (tp.binder eq from) tp.copyBoundType(to.asInstanceOf[tp.BT]) else tp @@ -163,11 +162,36 @@ object Substituters: .mapOver(tp) } - final class SubstBindingMap(from: BindingType, to: BindingType)(using Context) extends DeepTypeMap, BiTypeMap { + final class SubstBindingMap[BT <: BindingType](val from: BT, val to: BT)(using Context) extends DeepTypeMap, BiTypeMap { + override def fuse(next: BiTypeMap)(using Context) = next match + case next: SubstBindingMap[_] => + if next.from eq to then Some(SubstBindingMap(from, next.to)) + else Some(SubstBindingsMap(Array(from, next.from), Array(to, next.to))) + case _ => None def apply(tp: Type): Type = subst(tp, from, to, this)(using mapCtx) def inverse = SubstBindingMap(to, from) } + final class SubstBindingsMap(val from: Array[BindingType], val to: Array[BindingType])(using Context) extends DeepTypeMap, BiTypeMap { + override def fuse(next: BiTypeMap)(using Context) = next match + case next: SubstBindingMap[_] => + var i = 0 + while i < from.length && (to(i) ne next.from) do i += 1 + if i < from.length then Some(SubstBindingsMap(from, to.updated(i, next.to))) + else Some(SubstBindingsMap(from :+ next.from, to :+ next.to)) + case _ => None + + def apply(tp: Type): Type = tp match + case tp: BoundType => + var i = 0 + while i < from.length && (from(i) ne tp.binder) do i += 1 + if i < from.length then tp.copyBoundType(to(i).asInstanceOf[tp.BT]) else tp + case _ => + mapOver(tp) + + def inverse = SubstBindingsMap(to, from) + } + final class Subst1Map(from: Symbol, to: Type)(using Context) extends DeepTypeMap { def apply(tp: Type): Type = subst1(tp, from, to, this)(using mapCtx) } @@ -180,7 +204,7 @@ object Substituters: def apply(tp: Type): Type = subst(tp, from, to, this)(using mapCtx) } - final class SubstSymMap(from: List[Symbol], to: List[Symbol])(using Context) extends DeepTypeMap, BiTypeMap { + final class SubstSymMap(from: List[Symbol], to: List[Symbol])(using Context) extends DeepTypeMap { def apply(tp: Type): Type = substSym(tp, from, to, this)(using mapCtx) def inverse = SubstSymMap(to, from) // implicitly requires that `to` contains no duplicates. } @@ -189,15 +213,15 @@ object Substituters: def apply(tp: Type): Type = substThis(tp, from, to, this)(using mapCtx) } - final class SubstRecThisMap(from: Type, to: Type)(using Context) extends DeepTypeMap, IdempotentCaptRefMap { + final class SubstRecThisMap(from: Type, to: Type)(using Context) extends DeepTypeMap { def apply(tp: Type): Type = substRecThis(tp, from, to, this)(using mapCtx) } - final class SubstParamMap(from: ParamRef, to: Type)(using Context) extends DeepTypeMap, IdempotentCaptRefMap { + final class SubstParamMap(from: ParamRef, to: Type)(using Context) extends DeepTypeMap { def apply(tp: Type): Type = substParam(tp, from, to, this)(using mapCtx) } - final class SubstParamsMap(from: BindingType, to: List[Type])(using Context) extends DeepTypeMap, IdempotentCaptRefMap { + final class SubstParamsMap(from: BindingType, to: List[Type])(using Context) extends DeepTypeMap { def apply(tp: Type): Type = substParams(tp, from, to, this)(using mapCtx) } diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 54e18bf1ea1b..6c7b8dcef94a 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -806,6 +806,13 @@ object SymDenotations { final def isRealMethod(using Context): Boolean = this.is(Method, butNot = Accessor) && !isAnonymousFunction + /** A mutable variable (not a getter or setter for it) */ + final def isMutableVar(using Context): Boolean = is(Mutable, butNot = Method) + + /** A mutable variable or its getter or setter */ + final def isMutableVarOrAccessor(using Context): Boolean = + is(Mutable) && (!is(Method) || is(Accessor)) + /** Is this a getter? */ final def isGetter(using Context): Boolean = this.is(Accessor) && !originalName.isSetterName && !(originalName.isScala2LocalSuffix && symbol.owner.is(Scala2x)) @@ -1946,7 +1953,7 @@ object SymDenotations { case _ => NoSymbol /** The explicitly given self type (self types of modules are assumed to be - * explcitly given here). + * explicitly given here). */ def givenSelfType(using Context): Type = classInfo.selfInfo match { case tp: Type => tp diff --git a/compiler/src/dotty/tools/dotc/core/SymUtils.scala b/compiler/src/dotty/tools/dotc/core/SymUtils.scala index 54ba0e3bdd06..1b83014e5735 100644 --- a/compiler/src/dotty/tools/dotc/core/SymUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/SymUtils.scala @@ -287,7 +287,7 @@ class SymUtils: */ def isConstExprFinalVal(using Context): Boolean = atPhaseNoLater(erasurePhase) { - self.is(Final, butNot = Mutable) && self.info.resultType.isInstanceOf[ConstantType] + self.is(Final) && !self.isMutableVarOrAccessor && self.info.resultType.isInstanceOf[ConstantType] } && !self.sjsNeedsField /** The `ConstantType` of a val known to be `isConstrExprFinalVal`. diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 0139ff2c865e..b8a8be57ca05 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -47,8 +47,6 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling monitored = false GADTused = false opaquesUsed = false - openedExistentials = Nil - assocExistentials = Nil recCount = 0 needsGc = false if Config.checkTypeComparerReset then checkReset() @@ -67,18 +65,6 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling /** Indicates whether the subtype check used opaque types */ private var opaquesUsed: Boolean = false - /** In capture checking: The existential types that are open because they - * appear in an existential type on the left in an enclosing comparison. - */ - private var openedExistentials: List[TermParamRef] = Nil - - /** In capture checking: A map from existential types that are appear - * in an existential type on the right in an enclosing comparison. - * Each existential gets mapped to the opened existentials to which it - * may resolve at this point. - */ - private var assocExistentials: ExAssoc = Nil - private var myInstance: TypeComparer = this def currentInstance: TypeComparer = myInstance @@ -270,7 +256,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling report.log(explained(_.isSubType(tp1, tp2, approx), short = false)) } // Eliminate LazyRefs before checking whether we have seen a type before - val normalize = new TypeMap with CaptureSet.IdempotentCaptRefMap { + val normalize = new TypeMap { val DerefLimit = 10 var derefCount = 0 def apply(t: Type) = t match { @@ -335,10 +321,10 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // This is safe because X$ self-type is X.type sym1 = sym1.companionModule if (sym1 ne NoSymbol) && (sym1 eq sym2) then - ctx.erasedTypes || - sym1.isStaticOwner || - isSubPrefix(tp1.prefix, tp2.prefix) || - thirdTryNamed(tp2) + ctx.erasedTypes + || sym1.isStaticOwner + || isSubPrefix(tp1.prefix, tp2.prefix) + || thirdTryNamed(tp2) else (tp1.name eq tp2.name) && !sym1.is(Private) @@ -369,7 +355,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling } compareWild case tp2: LazyRef => - isBottom(tp1) || !tp2.evaluating && recur(tp1, tp2.ref) + isBottom(tp1) + || !tp2.evaluating && recur(tp1, tp2.ref) case CapturingType(_, _) => secondTry case tp2: AnnotatedType if !tp2.isRefining => @@ -439,7 +426,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling if (tp1.prefix.isStable) return tryLiftedToThis1 case _ => if isCaptureVarComparison then - return subCaptures(tp1.captureSet, tp2.captureSet, frozenConstraint).isOK + return CCState.withCapAsRoot: + subCaptures(tp1.captureSet, tp2.captureSet).isOK if (tp1 eq NothingType) || isBottom(tp1) then return true } @@ -492,7 +480,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // If `tp1` is in train of being evaluated, don't force it // because that would cause an assertionError. Return false instead. // See i859.scala for an example where we hit this case. - tp2.isRef(AnyClass, skipRefined = false) + tp2.isAny || !tp1.evaluating && recur(tp1.ref, tp2) case AndType(tp11, tp12) => if tp11.stripTypeVar eq tp12.stripTypeVar then recur(tp11, tp2) @@ -547,7 +535,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp1 @ CapturingType(parent1, refs1) => def compareCapturing = if tp2.isAny then true - else if subCaptures(refs1, tp2.captureSet, frozenConstraint).isOK && sameBoxed(tp1, tp2, refs1) + else if subCaptures(refs1, tp2.captureSet).isOK && sameBoxed(tp1, tp2, refs1) || !ctx.mode.is(Mode.CheckBoundsOrSelfType) && tp1.isAlwaysPure then val tp2a = @@ -564,8 +552,6 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling if reduced.exists then recur(reduced, tp2) && recordGadtUsageIf { MatchType.thatReducesUsingGadt(tp1) } else thirdTry - case Existential(boundVar, tp1unpacked) => - compareExistentialLeft(boundVar, tp1unpacked, tp2) case _: FlexType => true case _ => @@ -590,7 +576,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling && (isBottom(tp1) || GADTusage(tp2.symbol)) if isCaptureVarComparison then - return subCaptures(tp1.captureSet, tp2.captureSet, frozenConstraint).isOK + return CCState.withCapAsRoot: + subCaptures(tp1.captureSet, tp2.captureSet).isOK isSubApproxHi(tp1, info2.lo) && (trustBounds || isSubApproxHi(tp1, info2.hi)) || compareGADT @@ -650,8 +637,6 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling thirdTryNamed(tp2) case tp2: TypeParamRef => compareTypeParamRef(tp2) - case Existential(boundVar, tp2unpacked) => - compareExistentialRight(tp1, boundVar, tp2unpacked) case tp2: RefinedType => def compareRefinedSlow: Boolean = val name2 = tp2.refinedName @@ -677,12 +662,12 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling && isSubInfo(info1.resultType, info2.resultType.subst(info2, info1)) case (info1 @ CapturingType(parent1, refs1), info2: Type) if info2.stripCapturing.isInstanceOf[MethodOrPoly] => - subCaptures(refs1, info2.captureSet, frozenConstraint).isOK && sameBoxed(info1, info2, refs1) + subCaptures(refs1, info2.captureSet).isOK && sameBoxed(info1, info2, refs1) && isSubInfo(parent1, info2) case (info1: Type, CapturingType(parent2, refs2)) if info1.stripCapturing.isInstanceOf[MethodOrPoly] => val refs1 = info1.captureSet - (refs1.isAlwaysEmpty || subCaptures(refs1, refs2, frozenConstraint).isOK) && sameBoxed(info1, info2, refs1) + (refs1.isAlwaysEmpty || subCaptures(refs1, refs2).isOK) && sameBoxed(info1, info2, refs1) && isSubInfo(info1, parent2) case _ => isSubType(info1, info2) @@ -876,12 +861,12 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // capt-capibility.scala and function-combinators.scala val singletonOK = tp1 match case tp1: SingletonType - if subCaptures(tp1.underlying.captureSet, refs2, frozen = true).isOK => + if subCaptures(tp1.underlying.captureSet, refs2, CaptureSet.VarState.Separate).isOK => recur(tp1.widen, tp2) case _ => false singletonOK - || subCaptures(refs1, refs2, frozenConstraint).isOK + || subCaptures(refs1, refs2).isOK && sameBoxed(tp1, tp2, refs1) && (recur(tp1.widen.stripCapturing, parent2) || tp1.isInstanceOf[SingletonType] && recur(tp1, parent2) @@ -2173,7 +2158,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling val info2 = tp2.refinedInfo val isExpr2 = info2.isInstanceOf[ExprType] var info1 = m.info match - case info1: ValueType if isExpr2 || m.symbol.is(Mutable) => + case info1: ValueType if isExpr2 || m.symbol.isMutableVarOrAccessor => // OK: { val x: T } <: { def x: T } // OK: { var x: T } <: { def x: T } // NO: { var x: T } <: { val x: T } @@ -2805,119 +2790,24 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // ----------- Capture checking ----------------------------------------------- - /** A type associating instantiatable existentials on the right of a comparison - * with the existentials they can be instantiated with. - */ - type ExAssoc = List[(TermParamRef, List[TermParamRef])] - - private def compareExistentialLeft(boundVar: TermParamRef, tp1unpacked: Type, tp2: Type)(using Context): Boolean = - val saved = openedExistentials - try - openedExistentials = boundVar :: openedExistentials - recur(tp1unpacked, tp2) - finally - openedExistentials = saved - - private def compareExistentialRight(tp1: Type, boundVar: TermParamRef, tp2unpacked: Type)(using Context): Boolean = - val saved = assocExistentials - try - assocExistentials = (boundVar, openedExistentials) :: assocExistentials - recur(tp1, tp2unpacked) - finally - assocExistentials = saved - - /** Is `tp1` an existential var that subsumes `tp2`? This is the case if `tp1` is - * instantiatable (i.e. it's a key in `assocExistentials`) and one of the - * following is true: - * - `tp2` is not an existential var, - * - `tp1` is associated via `assocExistentials` with `tp2`, - * - `tp2` appears as key in `assocExistentials` further out than `tp1`. - * The third condition allows to instantiate c2 to c1 in - * EX c1: A -> Ex c2. B - */ - def subsumesExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context): Boolean = - def canInstantiateWith(assoc: ExAssoc): Boolean = assoc match - case (bv, bvs) :: assoc1 => - if bv == tp1 then - !Existential.isExistentialVar(tp2) - || bvs.contains(tp2) - || assoc1.exists(_._1 == tp2) - else - canInstantiateWith(assoc1) - case Nil => - false - Existential.isExistentialVar(tp1) && canInstantiateWith(assocExistentials) - - def isOpenedExistential(ref: CaptureRef)(using Context): Boolean = - openedExistentials.contains(ref) - - /** bi-map taking existentials to the left of a comparison to matching - * existentials on the right. This is not a bijection. However - * we have `forwards(backwards(bv)) == bv` for an existentially bound `bv`. - * That's enough to qualify as a BiTypeMap. - */ - private class MapExistentials(assoc: ExAssoc)(using Context) extends BiTypeMap: - - private def bad(t: Type) = - Existential.badExistential - .showing(i"existential match not found for $t in $assoc", capt) - - def apply(t: Type) = t match - case t: TermParamRef if Existential.isExistentialVar(t) => - // Find outermost existential on the right that can be instantiated to `t`, - // or `badExistential` if none exists. - def findMapped(assoc: ExAssoc): CaptureRef = assoc match - case (bv, assocBvs) :: assoc1 => - val outer = findMapped(assoc1) - if !Existential.isBadExistential(outer) then outer - else if assocBvs.contains(t) then bv - else bad(t) - case Nil => - bad(t) - findMapped(assoc) - case _ => - mapOver(t) - - /** The inverse takes existentials on the right to the innermost existential - * on the left to which they can be instantiated. - */ - lazy val inverse = new BiTypeMap: - def apply(t: Type) = t match - case t: TermParamRef if Existential.isExistentialVar(t) => - assoc.find(_._1 == t) match - case Some((_, bvs)) if bvs.nonEmpty => bvs.head - case _ => bad(t) - case _ => - mapOver(t) - - def inverse = MapExistentials.this - override def toString = "MapExistentials.inverse" - end inverse - end MapExistentials + protected def makeVarState() = + if frozenConstraint then CaptureSet.VarState.Closed() else CaptureSet.VarState() - protected def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = + protected def subCaptures(refs1: CaptureSet, refs2: CaptureSet, + vs: CaptureSet.VarState = makeVarState())(using Context): CaptureSet.CompareResult = try - if assocExistentials.isEmpty then - refs1.subCaptures(refs2, frozen) - else - val mapped = refs1.map(MapExistentials(assocExistentials)) - if mapped.elems.exists(Existential.isBadExistential) - then CaptureSet.CompareResult.Fail(refs2 :: Nil) - else subCapturesMapped(mapped, refs2, frozen) + refs1.subCaptures(refs2, vs) catch case ex: AssertionError => println(i"fail while subCaptures $refs1 <:< $refs2") throw ex - protected def subCapturesMapped(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = - refs1.subCaptures(refs2, frozen) - /** Is the boxing status of tp1 and tp2 the same, or alternatively, is * the capture sets `refs1` of `tp1` a subcapture of the empty set? * In the latter case, boxing status does not matter. */ protected def sameBoxed(tp1: Type, tp2: Type, refs1: CaptureSet)(using Context): Boolean = (tp1.isBoxedCapturing == tp2.isBoxedCapturing) - || refs1.subCaptures(CaptureSet.empty, frozenConstraint).isOK + || refs1.subCaptures(CaptureSet.empty, makeVarState()).isOK // ----------- Diagnostics -------------------------------------------------- @@ -3495,14 +3385,8 @@ object TypeComparer { def reduceMatchWith[T](op: MatchReducer => T)(using Context): T = comparing(_.reduceMatchWith(op)) - def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = - comparing(_.subCaptures(refs1, refs2, frozen)) - - def subsumesExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context) = - comparing(_.subsumesExistentially(tp1, tp2)) - - def isOpenedExistential(ref: CaptureRef)(using Context) = - comparing(_.isOpenedExistential(ref)) + def subCaptures(refs1: CaptureSet, refs2: CaptureSet, vs: CaptureSet.VarState)(using Context): CaptureSet.CompareResult = + comparing(_.subCaptures(refs1, refs2, vs)) } object MatchReducer: @@ -3977,14 +3861,9 @@ class ExplainingTypeComparer(initctx: Context, short: Boolean) extends TypeCompa super.gadtAddBound(sym, b, isUpper) } - override def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = - traceIndented(i"subcaptures $refs1 <:< $refs2 ${if frozen then "frozen" else ""}") { - super.subCaptures(refs1, refs2, frozen) - } - - override def subCapturesMapped(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = - traceIndented(i"subcaptures mapped $refs1 <:< $refs2 ${if frozen then "frozen" else ""}") { - super.subCapturesMapped(refs1, refs2, frozen) + override def subCaptures(refs1: CaptureSet, refs2: CaptureSet, vs: CaptureSet.VarState)(using Context): CaptureSet.CompareResult = + traceIndented(i"subcaptures $refs1 <:< $refs2 in ${vs.toString}") { + super.subCaptures(refs1, refs2, vs) } def lastTrace(header: String): String = header + { try b.toString finally b.clear() } diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 4c705c4252c0..0365b205c5b6 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -364,6 +364,8 @@ object TypeErasure { case tp: MatchType => val alts = tp.alternatives alts.nonEmpty && !fitsInJVMArray(alts.reduce(OrType(_, _, soft = true))) + case tp @ AppliedType(tycon, _) if tycon.isLambdaSub => + !fitsInJVMArray(tp.translucentSuperType) case tp: TypeProxy => isGenericArrayElement(tp.translucentSuperType, isScala2) case tp: AndType => @@ -781,11 +783,11 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst private def eraseArray(tp: Type)(using Context) = { val defn.ArrayOf(elemtp) = tp: @unchecked - if isGenericArrayElement(elemtp, isScala2 = sourceLanguage.isScala2) then + if isGenericArrayElement(elemtp, isScala2 = sourceLanguage.isScala2) then defn.ObjectType else if sourceLanguage.isScala2 && (elemtp.hiBound.isNullType || elemtp.hiBound.isNothingType) then JavaArrayType(defn.ObjectType) - else + else try erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, inSigName)(elemtp) match case _: WildcardType => WildcardType case elem => JavaArrayType(elem) diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index e3168ca5a27d..a1e26c20fdbb 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -19,7 +19,7 @@ import typer.Inferencing.* import typer.IfBottom import reporting.TestingReporter import cc.{CapturingType, derivedCapturingType, CaptureSet, captureSet, isBoxed, isBoxedCapturing} -import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} +import CaptureSet.{CompareResult, IdentityCaptRefMap, VarState} import scala.annotation.internal.sharable import scala.annotation.threadUnsafe @@ -56,7 +56,7 @@ object TypeOps: } /** The TypeMap handling the asSeenFrom */ - class AsSeenFromMap(pre: Type, cls: Symbol)(using Context) extends ApproximatingTypeMap, IdempotentCaptRefMap { + class AsSeenFromMap(pre: Type, cls: Symbol)(using Context) extends ApproximatingTypeMap { /** The number of range approximations in invariant or contravariant positions * performed by this TypeMap. @@ -124,7 +124,7 @@ object TypeOps: } def isLegalPrefix(pre: Type)(using Context): Boolean = - pre.isStable || !ctx.phase.isTyper + pre.isStable /** Implementation of Types#simplified */ def simplify(tp: Type, theMap: SimplifyMap | Null)(using Context): Type = { @@ -161,7 +161,7 @@ object TypeOps: TypeComparer.lub(simplify(l, theMap), simplify(r, theMap), isSoft = tp.isSoft) case tp @ CapturingType(parent, refs) => if !ctx.mode.is(Mode.Type) - && refs.subCaptures(parent.captureSet, frozen = true).isOK + && refs.subCaptures(parent.captureSet, VarState.Separate).isOK && (tp.isBoxed || !parent.isBoxedCapturing) // fuse types with same boxed status and outer boxed with any type then @@ -180,7 +180,7 @@ object TypeOps: if (normed.exists) simplify(normed, theMap) else mapOver case tp: MethodicType => // See documentation of `Types#simplified` - val addTypeVars = new TypeMap with IdempotentCaptRefMap: + val addTypeVars = new TypeMap: val constraint = ctx.typerState.constraint def apply(t: Type): Type = t match case t: TypeParamRef => constraint.typeVarOfParam(t).orElse(t) @@ -448,7 +448,7 @@ object TypeOps: } /** An approximating map that drops NamedTypes matching `toAvoid` and wildcard types. */ - abstract class AvoidMap(using Context) extends AvoidWildcardsMap, IdempotentCaptRefMap: + abstract class AvoidMap(using Context) extends AvoidWildcardsMap: @threadUnsafe lazy val localParamRefs = util.HashSet[Type]() def toAvoid(tp: NamedType): Boolean diff --git a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala index 739cc2b74a16..ab035db68d9f 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala @@ -68,7 +68,7 @@ class TypeUtils: def tupleElementTypesUpTo(bound: Int, normalize: Boolean = true)(using Context): Option[List[Type]] = def recur(tp: Type, bound: Int): Option[List[Type]] = if bound < 0 then Some(Nil) - else (if normalize then tp.normalized else tp).dealias match + else (if normalize then tp.dealias.normalized else tp).dealias match case AppliedType(tycon, hd :: tl :: Nil) if tycon.isRef(defn.PairClass) => recur(tl, bound - 1).map(hd :: _) case tp: AppliedType if defn.isTupleNType(tp) && normalize => diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 15b295af963e..2cd66464da3c 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -39,7 +39,7 @@ import reporting.{trace, Message} import java.lang.ref.WeakReference import compiletime.uninitialized import cc.* -import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} +import CaptureSet.{CompareResult, IdentityCaptRefMap} import scala.annotation.internal.sharable import scala.annotation.threadUnsafe @@ -98,12 +98,8 @@ object Types extends TypeUtils { // ----- Tests ----------------------------------------------------- // // debug only: a unique identifier for a type -// val uniqId = { -// nextId = nextId + 1 -// if (nextId == 19555) -// println("foo") -// nextId -// } +// val uniqId = { nextId = nextId + 1; nextId } +// if uniqId == 19555 then trace.dumpStack() /** A cache indicating whether the type was still provisional, last time we checked */ @sharable private var mightBeProvisional = true @@ -416,10 +412,9 @@ object Types extends TypeUtils { catch case ex: Throwable => handleRecursive("unusableForInference", show, ex) /** Does the type carry an annotation that is an instance of `cls`? */ - @tailrec final def hasAnnotation(cls: ClassSymbol)(using Context): Boolean = stripTypeVar match { - case AnnotatedType(tp, annot) => (annot matches cls) || (tp hasAnnotation cls) + @tailrec final def hasAnnotation(cls: ClassSymbol)(using Context): Boolean = stripTypeVar match + case AnnotatedType(tp, annot) => annot.matches(cls) || tp.hasAnnotation(cls) case _ => false - } /** Returns the annotation that is an instance of `cls` carried by the type. */ @tailrec final def getAnnotation(cls: ClassSymbol)(using Context): Option[Annotation] = stripTypeVar match { @@ -588,8 +583,8 @@ object Types extends TypeUtils { case AndType(l, r) => val lsym = l.classSymbol val rsym = r.classSymbol - if (lsym isSubClass rsym) lsym - else if (rsym isSubClass lsym) rsym + if lsym.isSubClass(rsym) then lsym + else if rsym.isSubClass(lsym) then rsym else NoSymbol case tp: OrType => if tp.tp1.hasClassSymbol(defn.NothingClass) then @@ -728,7 +723,7 @@ object Types extends TypeUtils { case tp: TypeProxy => tp.superType.findDecl(name, excluded) case err: ErrorType => - newErrorSymbol(classSymbol orElse defn.RootClass, name, err.msg) + newErrorSymbol(classSymbol.orElse(defn.RootClass), name, err.msg) case _ => NoDenotation } @@ -819,7 +814,7 @@ object Types extends TypeUtils { case tp: JavaArrayType => defn.ObjectType.findMember(name, pre, required, excluded) case err: ErrorType => - newErrorSymbol(pre.classSymbol orElse defn.RootClass, name, err.msg) + newErrorSymbol(pre.classSymbol.orElse(defn.RootClass), name, err.msg) case _ => NoDenotation } @@ -865,20 +860,27 @@ object Types extends TypeUtils { pdenot.asSingleDenotation.derivedSingleDenotation(pdenot.symbol, jointInfo) } else - val isRefinedMethod = rinfo.isInstanceOf[MethodOrPoly] - val joint = pdenot.meet( - new JointRefDenotation(NoSymbol, rinfo, Period.allInRun(ctx.runId), pre, isRefinedMethod), - pre, - safeIntersection = ctx.base.pendingMemberSearches.contains(name)) - joint match - case joint: SingleDenotation - if isRefinedMethod - && (rinfo <:< joint.info - || name == nme.apply && defn.isFunctionType(tp.parent)) => - // use `rinfo` to keep the right parameter names for named args. See i8516.scala. - joint.derivedSingleDenotation(joint.symbol, rinfo, pre, isRefinedMethod) - case _ => - joint + val overridingRefinement = rinfo match + case AnnotatedType(rinfo1, ann) if ann.symbol == defn.RefineOverrideAnnot => rinfo1 + case _ if pdenot.symbol.is(Tracked) => rinfo + case _ => NoType + if overridingRefinement.exists then + pdenot.asSingleDenotation.derivedSingleDenotation(pdenot.symbol, overridingRefinement) + else + val isRefinedMethod = rinfo.isInstanceOf[MethodOrPoly] + val joint = pdenot.meet( + new JointRefDenotation(NoSymbol, rinfo, Period.allInRun(ctx.runId), pre, isRefinedMethod), + pre, + safeIntersection = ctx.base.pendingMemberSearches.contains(name)) + joint match + case joint: SingleDenotation + if isRefinedMethod + && (rinfo <:< joint.info + || name == nme.apply && defn.isFunctionType(tp.parent)) => + // use `rinfo` to keep the right parameter names for named args. See i8516.scala. + joint.derivedSingleDenotation(joint.symbol, rinfo, pre, isRefinedMethod) + case _ => + joint } def goApplied(tp: AppliedType, tycon: HKTypeLambda) = @@ -916,7 +918,7 @@ object Types extends TypeUtils { // member in Super instead of Sub. // As an example of this in the wild, see // loadClassWithPrivateInnerAndSubSelf in ShowClassTests - go(tp.cls.typeRef) orElse d + go(tp.cls.typeRef).orElse(d) def goParam(tp: TypeParamRef) = { val next = tp.underlying @@ -1125,7 +1127,7 @@ object Types extends TypeUtils { TypeComparer.topLevelSubType(this, that) } - /** Is this type a subtype of that type? */ + /** Is this type a subtype of that type without adding to the constraint? */ final def frozen_<:<(that: Type)(using Context): Boolean = { record("frozen_<:<") TypeComparer.isSubTypeWhenFrozen(this, that) @@ -1154,7 +1156,7 @@ object Types extends TypeUtils { false def relaxed_<:<(that: Type)(using Context): Boolean = - (this <:< that) || (this isValueSubType that) + (this <:< that) || this.isValueSubType(that) /** Is this type a legal type for member `sym1` that overrides another * member `sym2` of type `that`? This is the same as `<:<`, except that @@ -1164,10 +1166,9 @@ object Types extends TypeUtils { * * @param isSubType a function used for checking subtype relationships. */ - final def overrides(that: Type, matchLoosely: => Boolean, checkClassInfo: Boolean = true, - isSubType: (Type, Type) => Context ?=> Boolean = (tp1, tp2) => tp1 frozen_<:< tp2)(using Context): Boolean = { + final def overrides(that: Type, matchLoosely: => Boolean, checkClassInfo: Boolean = true)(using Context): Boolean = { !checkClassInfo && this.isInstanceOf[ClassInfo] - || isSubType(this.widenExpr, that.widenExpr) + || (this.widenExpr frozen_<:< that.widenExpr) || matchLoosely && { val this1 = this.widenNullaryMethod val that1 = that.widenNullaryMethod @@ -1205,10 +1206,10 @@ object Types extends TypeUtils { * vice versa. */ def matchesLoosely(that: Type)(using Context): Boolean = - (this matches that) || { + this.matches(that) || { val thisResult = this.widenExpr val thatResult = that.widenExpr - (this eq thisResult) != (that eq thatResult) && (thisResult matchesLoosely thatResult) + (this eq thisResult) != (that eq thatResult) && thisResult.matchesLoosely(thatResult) } /** The basetype of this type with given class symbol, NoType if `base` is not a class. */ @@ -1866,7 +1867,7 @@ object Types extends TypeUtils { * no symbol it tries `member` as an alternative. */ def typeParamNamed(name: TypeName)(using Context): Symbol = - classSymbol.unforcedDecls.lookup(name) orElse member(name).symbol + classSymbol.unforcedDecls.lookup(name).orElse(member(name).symbol) /** If this is a prototype with some ignored component, reveal one more * layer of it. Otherwise the type itself. @@ -2005,9 +2006,9 @@ object Types extends TypeUtils { def annotatedToRepeated(using Context): Type = this match { case tp @ ExprType(tp1) => tp.derivedExprType(tp1.annotatedToRepeated) - case self @ AnnotatedType(tp, annot) if annot matches defn.RetainsByNameAnnot => + case self @ AnnotatedType(tp, annot) if annot.matches(defn.RetainsByNameAnnot) => self.derivedAnnotatedType(tp.annotatedToRepeated, annot) - case AnnotatedType(tp, annot) if annot matches defn.RepeatedAnnot => + case AnnotatedType(tp, annot) if annot.matches(defn.RepeatedAnnot) => val typeSym = tp.typeSymbol.asClass assert(typeSym == defn.SeqClass || typeSym == defn.ArrayClass) tp.translateParameterized(typeSym, defn.RepeatedParamClass) @@ -2703,9 +2704,9 @@ object Types extends TypeUtils { */ final def controlled[T](op: => T)(using Context): T = try { ctx.base.underlyingRecursions += 1 - if (ctx.base.underlyingRecursions < Config.LogPendingUnderlyingThreshold) + if ctx.base.underlyingRecursions < Config.LogPendingUnderlyingThreshold then op - else if (ctx.pendingUnderlying contains this) + else if ctx.pendingUnderlying.contains(this) then throw CyclicReference(symbol) else try { @@ -3462,8 +3463,8 @@ object Types extends TypeUtils { val bcs1set = BaseClassSet(bcs1) def recur(bcs2: List[ClassSymbol]): List[ClassSymbol] = bcs2 match { case bc2 :: bcs2rest => - if (bcs1set contains bc2) - if (bc2.is(Trait)) recur(bcs2rest) + if bcs1set.contains(bc2) then + if bc2.is(Trait) then recur(bcs2rest) else bcs1 // common class, therefore rest is the same in both sequences else bc2 :: recur(bcs2rest) case nil => bcs1 @@ -3559,9 +3560,8 @@ object Types extends TypeUtils { val bcs1set = BaseClassSet(bcs1) def recur(bcs2: List[ClassSymbol]): List[ClassSymbol] = bcs2 match { case bc2 :: bcs2rest => - if (bcs1set contains bc2) - if (bc2.is(Trait)) bc2 :: recur(bcs2rest) - else bcs2 + if bcs1set.contains(bc2) then + if bc2.is(Trait) then bc2 :: recur(bcs2rest) else bcs2 else recur(bcs2rest) case nil => bcs2 @@ -3820,9 +3820,51 @@ object Types extends TypeUtils { def integrate(tparams: List[ParamInfo], tp: Type)(using Context): Type = (tparams: @unchecked) match { case LambdaParam(lam, _) :: _ => tp.subst(lam, this) // This is where the precondition is necessary. - case params: List[Symbol @unchecked] => tp.subst(params, paramRefs) + case params: List[Symbol @unchecked] => IntegrateMap(params, paramRefs)(tp) } + /** A map that replaces references to symbols in `params` by the types in + * `paramRefs`. + * + * It is similar to [[Substituters#subst]] but avoids reloading denotations + * of named types by overriding `derivedSelect`. + * + * This is needed because during integration, [[TermParamRef]]s refer to a + * [[LambdaType]] that is not yet fully constructed, in particular for wich + * `paramInfos` is `null`. In that case all [[TermParamRef]]s have + * [[NoType]] as underlying type. Reloading denotions of selections + * involving such [[TermParamRef]]s in [[NamedType#withPrefix]] could then + * result in a [[NoDenotation]], which would make later disambiguation of + * overloads impossible. See `tests/pos/annot-17242.scala` for example. + */ + private class IntegrateMap(from: List[Symbol], to: List[Type])(using Context) extends TypeMap: + override def apply(tp: Type) = + // Same implementation as in `SubstMap`, except the `derivedSelect` in + // the `NamedType` case, and the default case that just calls `mapOver`. + tp match + case tp: NamedType => + val sym = tp.symbol + var fs = from + var ts = to + while (fs.nonEmpty && ts.nonEmpty) { + if (fs.head eq sym) return ts.head + fs = fs.tail + ts = ts.tail + } + if (tp.prefix `eq` NoPrefix) tp + else derivedSelect(tp, apply(tp.prefix)) + case _: BoundType | _: ThisType => tp + case _ => mapOver(tp) + + override final def derivedSelect(tp: NamedType, pre: Type): Type = + if tp.prefix eq pre then tp + else + pre match + case ref: ParamRef if (ref.binder eq self) && tp.symbol.exists => + NamedType(pre, tp.name, tp.denot.asSeenFrom(pre)) + case _ => + tp.derivedSelect(pre) + final def derivedLambdaType(paramNames: List[ThisName] = this.paramNames, paramInfos: List[PInfo] = this.paramInfos, resType: Type = this.resType)(using Context): This = @@ -3953,7 +3995,7 @@ object Types extends TypeUtils { def apply(tp: Type) = tp match { case tp @ TypeRef(pre, _) => tp.info match { - case TypeAlias(alias) if depStatus(NoDeps, pre) == TrueDeps => apply(alias) + case TypeAlias(alias) if depStatus(NoDeps, pre, forParams = false) == TrueDeps => apply(alias) case _ => mapOver(tp) } case _ => @@ -3967,7 +4009,7 @@ object Types extends TypeUtils { private var myDependencyStatus: DependencyStatus = Unknown private var myParamDependencyStatus: DependencyStatus = Unknown - private def depStatus(initial: DependencyStatus, tp: Type)(using Context): DependencyStatus = + private def depStatus(initial: DependencyStatus, tp: Type, forParams: Boolean)(using Context): DependencyStatus = class DepAcc extends TypeAccumulator[DependencyStatus]: def apply(status: DependencyStatus, tp: Type) = compute(status, tp, this) def combine(x: DependencyStatus, y: DependencyStatus) = @@ -3996,11 +4038,13 @@ object Types extends TypeUtils { case tp: AnnotatedType => tp match case CapturingType(parent, refs) => - (compute(status, parent, theAcc) /: refs.elems) { + val status1 = (compute(status, parent, theAcc) /: refs.elems): (s, ref) => ref.stripReach match - case tp: TermParamRef if tp.binder eq thisLambdaType => combine(s, CaptureDeps) + case tp: TermParamRef if tp.binder eq thisLambdaType => combine(s, TrueDeps) case tp => combine(s, compute(status, tp, theAcc)) - } + if refs.isConst || forParams // We assume capture set variables in parameters don't generate param dependencies + then status1 + else combine(status1, Provisional) case _ => if tp.annot.refersToParamOf(thisLambdaType) then TrueDeps else compute(status, tp.parent, theAcc) @@ -4024,7 +4068,7 @@ object Types extends TypeUtils { private def dependencyStatus(using Context): DependencyStatus = if (myDependencyStatus != Unknown) myDependencyStatus else { - val result = depStatus(NoDeps, resType) + val result = depStatus(NoDeps, resType, forParams = false) if ((result & Provisional) == 0) myDependencyStatus = result (result & StatusMask).toByte } @@ -4037,7 +4081,7 @@ object Types extends TypeUtils { else { val result = if (paramInfos.isEmpty) NoDeps - else paramInfos.tail.foldLeft(NoDeps)(depStatus(_, _)) + else paramInfos.tail.foldLeft(NoDeps)(depStatus(_, _, forParams = true)) if ((result & Provisional) == 0) myParamDependencyStatus = result (result & StatusMask).toByte } @@ -4046,25 +4090,28 @@ object Types extends TypeUtils { * which cannot be eliminated by de-aliasing? */ def isResultDependent(using Context): Boolean = - dependencyStatus == TrueDeps || dependencyStatus == CaptureDeps + dependencyStatus == TrueDeps /** Does one of the parameter types contain references to earlier parameters * of this method type which cannot be eliminated by de-aliasing? */ def isParamDependent(using Context): Boolean = - paramDependencyStatus == TrueDeps || paramDependencyStatus == CaptureDeps + paramDependencyStatus == TrueDeps - /** Is there a dependency involving a reference in a capture set, but - * otherwise no true result dependency? - */ - def isCaptureDependent(using Context) = dependencyStatus == CaptureDeps + /** Like isResultDependent, but without attempt to eliminate dependencies with de-aliasing */ + def looksResultDependent(using Context): Boolean = + (dependencyStatus & StatusMask) != NoDeps + + /** Like isParamDependent, but without attempt to eliminate dependencies with de-aliasing */ + def looksParamDependent(using Context): Boolean = + (paramDependencyStatus & StatusMask) != NoDeps def newParamRef(n: Int): TermParamRef = new TermParamRefImpl(this, n) /** The least supertype of `resultType` that does not contain parameter dependencies */ def nonDependentResultApprox(using Context): Type = if isResultDependent then - val dropDependencies = new ApproximatingTypeMap with IdempotentCaptRefMap { + val dropDependencies = new ApproximatingTypeMap { def apply(tp: Type) = tp match { case tp @ TermParamRef(`thisLambdaType`, _) => range(defn.NothingType, atVariance(1)(apply(tp.underlying))) @@ -4073,7 +4120,7 @@ object Types extends TypeUtils { case ReachCapability(tp1) => apply(tp1) match case tp1a: CaptureRef if tp1a.isTrackableRef => tp1a.reach - case _ => defn.captureRoot.termRef + case _ => root.cap case AnnotatedType(parent, ann) if ann.refersToParamOf(thisLambdaType) => val parent1 = mapOver(parent) if ann.symbol.isRetainsLike then @@ -4087,6 +4134,10 @@ object Types extends TypeUtils { } dropDependencies(resultType) else resultType + + /** Are all parameter names synthetic? */ + def allParamNamesSynthetic = paramNames.zipWithIndex.forall: (name, i) => + name == nme.syntheticParamName(i) } abstract case class MethodType(paramNames: List[TermName])( @@ -4116,7 +4167,6 @@ object Types extends TypeUtils { def nonErasedParamCount(using Context): Int = paramInfos.count(p => !p.hasAnnotation(defn.ErasedParamAnnot)) - protected def prefixString: String = companion.prefixString } @@ -4179,7 +4229,7 @@ object Types extends TypeUtils { tl => params.map(p => tl.integrate(params, adaptParamInfo(p))), tl => tl.integrate(params, resultType)) - /** Adapt info of parameter symbol to be integhrated into corresponding MethodType + /** Adapt info of parameter symbol to be integrated into corresponding MethodType * using the scheme described in `fromSymbols`. */ def adaptParamInfo(param: Symbol, pinfo: Type)(using Context): Type = @@ -4210,6 +4260,35 @@ object Types extends TypeUtils { } mt } + + /** Not safe to use in general: Check that all references to an enclosing + * TermParamRef name point to that TermParamRef + */ + def checkValid2(mt: MethodType)(using Context): mt.type = { + var t = new TypeTraverser: + val ps = mt.paramNames.zip(mt.paramRefs).toMap + def traverse(t: Type) = + t match + case CapturingType(p, refs) => + def checkRefs(refs: CaptureSet) = + for elem <- refs.elems do + elem match + case elem: TermParamRef => + val elemName = elem.binder.paramNames(elem.paramNum) + //assert(elemName.toString != "f") + ps.get(elemName) match + case Some(elemRef) => assert(elemRef eq elem, i"bad $mt") + case _ => + case root.Result(binder) if binder ne mt => + assert(binder.paramNames.toList != mt.paramNames.toList, i"bad $mt") + case _ => + checkRefs(refs) + traverse(p) + case _ => + traverseChildren(t) + t.traverse(mt.resType) + mt + } } object MethodType extends MethodTypeCompanion("MethodType") { @@ -4465,8 +4544,7 @@ object Types extends TypeUtils { final val Unknown: DependencyStatus = 0 // not yet computed final val NoDeps: DependencyStatus = 1 // no dependent parameters found final val FalseDeps: DependencyStatus = 2 // all dependent parameters are prefixes of non-depended alias types - final val CaptureDeps: DependencyStatus = 3 // dependencies in capture sets under captureChecking, otherwise only false dependencoes - final val TrueDeps: DependencyStatus = 4 // some truly dependent parameters exist + final val TrueDeps: DependencyStatus = 3 // some truly dependent parameters exist final val StatusMask: DependencyStatus = 7 // the bits indicating actual dependency status final val Provisional: DependencyStatus = 8 // set if dependency status can still change due to type variable instantiations } @@ -4707,7 +4785,7 @@ object Types extends TypeUtils { override def hashIsStable: Boolean = false } - abstract class ParamRef extends BoundType { + abstract class ParamRef extends BoundType, CaptureRef { type BT <: LambdaType def paramNum: Int def paramName: binder.ThisName = binder.paramNames(paramNum) @@ -4754,7 +4832,7 @@ object Types extends TypeUtils { * refer to `TypeParamRef(binder, paramNum)`. */ abstract case class TypeParamRef(binder: TypeLambda, paramNum: Int) - extends ParamRef, CaptureRef { + extends ParamRef { type BT = TypeLambda def kindString: String = "Type" def copyBoundType(bt: BT): Type = bt.paramRefs(paramNum) @@ -5579,24 +5657,25 @@ object Types extends TypeUtils { } def & (that: TypeBounds)(using Context): TypeBounds = + val lo1 = this.lo.stripLazyRef + val lo2 = that.lo.stripLazyRef + val hi1 = this.hi.stripLazyRef + val hi2 = that.hi.stripLazyRef + // This will try to preserve the FromJavaObjects type in upper bounds. // For example, (? <: FromJavaObjects | Null) & (? <: Any), // we want to get (? <: FromJavaObjects | Null) intead of (? <: Any), // because we may check the result <:< (? <: Object | Null) later. - if this.hi.containsFromJavaObject - && (this.hi frozen_<:< that.hi) - && (that.lo frozen_<:< this.lo) then + if hi1.containsFromJavaObject && (hi1 frozen_<:< hi2) && (lo2 frozen_<:< lo1) then // FromJavaObject in tp1.hi guarantees tp2.hi <:< tp1.hi // prefer tp1 if FromJavaObject is in its hi this - else if that.hi.containsFromJavaObject - && (that.hi frozen_<:< this.hi) - && (this.lo frozen_<:< that.lo) then + else if hi2.containsFromJavaObject && (hi2 frozen_<:< hi1) && (lo1 frozen_<:< lo2) then // Similarly, prefer tp2 if FromJavaObject is in its hi that - else if (this.lo frozen_<:< that.lo) && (that.hi frozen_<:< this.hi) then that - else if (that.lo frozen_<:< this.lo) && (this.hi frozen_<:< that.hi) then this - else TypeBounds(this.lo | that.lo, this.hi & that.hi) + else if (lo1 frozen_<:< lo2) && (hi2 frozen_<:< hi1) then that + else if (lo2 frozen_<:< lo1) && (hi1 frozen_<:< hi2) then this + else TypeBounds(lo1 | lo2, hi1 & hi2) def | (that: TypeBounds)(using Context): TypeBounds = if ((this.lo frozen_<:< that.lo) && (that.hi frozen_<:< this.hi)) this @@ -5605,7 +5684,7 @@ object Types extends TypeUtils { override def & (that: Type)(using Context): Type = that match { case that: TypeBounds => this & that - case _ => super.& (that) + case _ => super.&(that) } override def | (that: Type)(using Context): Type = that match { @@ -5747,11 +5826,11 @@ object Types extends TypeUtils { parent.hashIsStable override def eql(that: Type): Boolean = that match - case that: AnnotatedType => (parent eq that.parent) && (annot eql that.annot) + case that: AnnotatedType => (parent eq that.parent) && annot.eql(that.annot) case _ => false override def iso(that: Any, bs: BinderPairs): Boolean = that match - case that: AnnotatedType => parent.equals(that.parent, bs) && (annot eql that.annot) + case that: AnnotatedType => parent.equals(that.parent, bs) && annot.eql(that.annot) case _ => false } @@ -6076,24 +6155,22 @@ object Types extends TypeUtils { def forward(ref: CaptureRef): CaptureRef = val result = this(ref) def ensureTrackable(tp: Type): CaptureRef = tp match - /* Issue #22437: handle case when info is not yet available during postProcess in CC setup */ - case tp: (TypeParamRef | TermRef) if tp.underlying == NoType => - tp case tp: CaptureRef => if tp.isTrackableRef then tp else ensureTrackable(tp.underlying) case tp: TypeAlias => ensureTrackable(tp.alias) case _ => - assert(false, i"not a trackable captureRef ref: $result, ${result.underlyingIterator.toList}") + assert(false, i"not a trackable CaptureRef: $result with underlying ${result.underlyingIterator.toList}") ensureTrackable(result) /** A restriction of the inverse to a function on tracked CaptureRefs */ def backward(ref: CaptureRef): CaptureRef = inverse(ref) match - /* Ensure bijection for issue #22437 fix in method forward above: */ - case result: (TypeParamRef | TermRef) if result.underlying == NoType => - result case result: CaptureRef if result.isTrackableRef => result + + /** Fuse with another map */ + def fuse(next: BiTypeMap)(using Context): Option[TypeMap] = None + end BiTypeMap abstract class TypeMap(implicit protected var mapCtx: Context) @@ -6300,14 +6377,7 @@ object Types extends TypeUtils { } } - private def treeTypeMap = new TreeTypeMap( - typeMap = this, - // Using `ConservativeTreeCopier` is needed when copying dependent annoted - // types, where we can refer to a previous parameter represented as - // `TermParamRef` that has no underlying type yet. - // See tests/pos/annot-17242.scala. - cpy = ConservativeTreeCopier() - ) + private def treeTypeMap = new TreeTypeMap(typeMap = this) def mapOver(syms: List[Symbol]): List[Symbol] = mapSymbols(syms, treeTypeMap) @@ -6344,7 +6414,7 @@ object Types extends TypeUtils { tp.derivedClassInfo(prefix1, parents1, tp.decls, selfInfo1) end DeepTypeMap - @sharable object IdentityTypeMap extends TypeMap()(NoContext) { + @sharable object IdentityTypeMap extends TypeMap()(using NoContext) { def apply(tp: Type): Type = tp } @@ -6817,7 +6887,7 @@ object Types extends TypeUtils { def maybeAdd(xs: List[NamedType], tp: NamedType): List[NamedType] = if p(tp) then tp :: xs else xs val seen = util.HashSet[Type]() def apply(xs: List[NamedType], tp: Type): List[NamedType] = - if seen contains tp then xs + if seen.contains(tp) then xs else seen += tp tp match @@ -6996,7 +7066,7 @@ object Types extends TypeUtils { object fieldFilter extends NameFilter { def apply(pre: Type, name: Name)(using Context): Boolean = - name.isTermName && (pre member name).hasAltWith(!_.symbol.is(Method)) + name.isTermName && pre.member(name).hasAltWith(!_.symbol.is(Method)) def isStable = true } diff --git a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala index 92cae663352a..0268ae2b2f58 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala @@ -96,6 +96,15 @@ object Inliner: } end isElideableExpr + // InlineCopier is a more fault-tolerant copier that does not cause errors when + // function types in applications are undefined. This is necessary since we copy at + // the same time as establishing the proper context in which the copied tree should + // be evaluated. This matters for opaque types, see neg/i14653.scala. + private class InlineCopier() extends TypedTreeCopier: + override def Apply(tree: Tree)(fun: Tree, args: List[Tree])(using Context): Apply = + if fun.tpe.widen.exists then super.Apply(tree)(fun, args) + else untpd.cpy.Apply(tree)(fun, args).withTypeUnchecked(tree.tpe) + // InlinerMap is a TreeTypeMap with special treatment for inlined arguments: // They are generally left alone (not mapped further, and if they wrap a type // the type Inlined wrapper gets dropped. @@ -108,13 +117,7 @@ object Inliner: substFrom: List[Symbol], substTo: List[Symbol])(using Context) extends TreeTypeMap( - typeMap, treeMap, oldOwners, newOwners, substFrom, substTo, - // It is necessary to use the `ConservativeTreeCopier` since we copy at - // the same time as establishing the proper context in which the copied - // tree should be evaluated. This matters for opaque types, see - // neg/i14653.scala. - ConservativeTreeCopier() - ): + typeMap, treeMap, oldOwners, newOwners, substFrom, substTo, InlineCopier()): override def transform(tree: Tree)(using Context): Tree = tree match @@ -162,28 +165,9 @@ object Inliner: else Nil case _ => Nil val refinements = openOpaqueAliases(cls.givenSelfType) - - // Map references in the refinements from the proxied termRef - // to the recursive type of the refined type - // e.g.: Obj.type{type A = Obj.B; type B = Int} -> Obj.type{type A = .B; type B = Int} - def mapRecTermRefReferences(recType: RecType, refinedType: Type) = - new TypeMap { - def apply(tp: Type) = tp match - case RefinedType(a: RefinedType, b, info) => RefinedType(apply(a), b, apply(info)) - case RefinedType(a, b, info) => RefinedType(a, b, apply(info)) - case TypeRef(prefix, des) => TypeRef(apply(prefix), des) - case termRef: TermRef if termRef == ref => recType.recThis - case _ => mapOver(tp) - }.apply(refinedType) - val refinedType = refinements.foldLeft(ref: Type): (parent, refinement) => RefinedType(parent, refinement._1, TypeAlias(refinement._2)) - - val recType = RecType.closeOver ( recType => - mapRecTermRefReferences(recType, refinedType) - ) - - val refiningSym = newSym(InlineBinderName.fresh(), Synthetic, recType, span) + val refiningSym = newSym(InlineBinderName.fresh(), Synthetic, refinedType, span) refiningSym.termRef def unapply(refiningRef: TermRef)(using Context): Option[TermRef] = @@ -402,9 +386,6 @@ class Inliner(val call: tpd.Tree)(using Context): */ private val opaqueProxies = new mutable.ListBuffer[(TermRef, TermRef)] - /** TermRefs for which we already started synthesising proxies */ - private val visitedTermRefs = new mutable.HashSet[TermRef] - protected def hasOpaqueProxies = opaqueProxies.nonEmpty /** Map first halves of opaqueProxies pairs to second halves, using =:= as equality */ @@ -432,15 +413,12 @@ class Inliner(val call: tpd.Tree)(using Context): for cls <- ref.widen.baseClasses do if cls.containsOpaques && (forThisProxy || inlinedMethod.isContainedIn(cls)) - && !visitedTermRefs.contains(ref) + && mapRef(ref).isEmpty then - visitedTermRefs += ref val refiningRef = OpaqueProxy(ref, cls, call.span) val refiningSym = refiningRef.symbol.asTerm val refinedType = refiningRef.info - val refiningDef = addProxiesForRecurrentOpaques( - ValDef(refiningSym, tpd.ref(ref).cast(refinedType), inferred = true).withSpan(span) - ) + val refiningDef = ValDef(refiningSym, tpd.ref(ref).cast(refinedType), inferred = true).withSpan(span) inlining.println(i"add opaque alias proxy $refiningDef for $ref in $tp") bindingsBuf += refiningDef opaqueProxies += ((ref, refiningSym.termRef)) @@ -460,27 +438,6 @@ class Inliner(val call: tpd.Tree)(using Context): } ) - /** Transforms proxies that reference other opaque types, like for: - * object Obj1 { opaque type A = Int } - * object Obj2 { opaque type B = A } - * and proxy$1 of type Obj2.type{type B = Obj1.A} - * creates proxy$2 of type Obj1.type{type A = Int} - * and transforms proxy$1 into Obj2.type{type B = proxy$2.A} - */ - private def addProxiesForRecurrentOpaques(binding: ValDef)(using Context): ValDef = - def fixRefinedTypes(ref: Type): Unit = - ref match - case recType: RecType => fixRefinedTypes(recType.underlying) - case RefinedType(parent, name, info) => - addOpaqueProxies(info.widen, binding.span, true) - fixRefinedTypes(parent) - case _ => - fixRefinedTypes(binding.symbol.info) - binding.symbol.info = mapOpaques.typeMap(binding.symbol.info) - mapOpaques.transform(binding).asInstanceOf[ValDef] - .showing(i"transformed this binding exposing opaque aliases: $result", inlining) - end addProxiesForRecurrentOpaques - /** If `binding` contains TermRefs that refer to objects with opaque * type aliases, add proxy definitions that expose these aliases * and substitute such TermRefs with theproxies. Example from pos/opaque-inline1.scala: diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 85bea871b955..a7269c83bccb 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -395,6 +395,11 @@ object Inlines: case ConstantType(Constant(code: String)) => val unitName = "tasty-reflect" val source2 = SourceFile.virtual(unitName, code) + def compilationUnits(untpdTree: untpd.Tree, tpdTree: Tree): List[CompilationUnit] = + val compilationUnit = CompilationUnit(unitName, code) + compilationUnit.tpdTree = tpdTree + compilationUnit.untpdTree = untpdTree + List(compilationUnit) // We need a dummy owner, as the actual one does not have a computed denotation yet, // but might be inspected in a transform phase, leading to cyclic errors val dummyOwner = newSymbol(ctx.owner, "$dummySymbol$".toTermName, Private, defn.AnyType, NoSymbol) @@ -407,31 +412,30 @@ object Inlines: .withOwner(dummyOwner) inContext(newContext) { - val tree2 = new Parser(source2).block() - if ctx.reporter.allErrors.nonEmpty then + def noErrors = ctx.reporter.allErrors.isEmpty + val untpdTree = new Parser(source2).block() + if !noErrors then ctx.reporter.allErrors.map((ErrorKind.Parser, _)) else - val tree3 = ctx.typer.typed(tree2) + val tpdTree1 = ctx.typer.typed(untpdTree) ctx.base.postTyperPhase match - case postTyper: PostTyper if ctx.reporter.allErrors.isEmpty => - val tree4 = atPhase(postTyper) { postTyper.newTransformer.transform(tree3) } + case postTyper: PostTyper if noErrors => + val tpdTree2 = + atPhase(postTyper) { postTyper.runOn(compilationUnits(untpdTree, tpdTree1)).head.tpdTree } ctx.base.setRootTreePhase match - case setRootTree => - val tree5 = - val compilationUnit = CompilationUnit(unitName, code) - compilationUnit.tpdTree = tree4 - compilationUnit.untpdTree = tree2 - var units = List(compilationUnit) - atPhase(setRootTree)(setRootTree.runOn(units).head.tpdTree) + case setRootTree if noErrors => // might be noPhase, if -Yretain-trees is not used + val tpdTree3 = + atPhase(setRootTree)(setRootTree.runOn(compilationUnits(untpdTree, tpdTree2)).head.tpdTree) ctx.base.inliningPhase match - case inlining: Inlining if ctx.reporter.allErrors.isEmpty => - val tree6 = atPhase(inlining) { inlining.newTransformer.transform(tree5) } - if ctx.reporter.allErrors.isEmpty && reconstructedTransformPhases.nonEmpty then - var transformTree = tree6 + case inlining: Inlining if noErrors => + val tpdTree4 = atPhase(inlining) { inlining.newTransformer.transform(tpdTree3) } + if noErrors && reconstructedTransformPhases.nonEmpty then + var transformTree = tpdTree4 for phase <- reconstructedTransformPhases do - if ctx.reporter.allErrors.isEmpty then + if noErrors then transformTree = atPhase(phase.end + 1)(phase.transformUnit(transformTree)) case _ => + case _ => case _ => ctx.reporter.allErrors.map((ErrorKind.Typer, _)) } diff --git a/compiler/src/dotty/tools/dotc/interactive/Completion.scala b/compiler/src/dotty/tools/dotc/interactive/Completion.scala index e59a8e0b882d..36f6fa18e94b 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Completion.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Completion.scala @@ -48,6 +48,16 @@ case class Completion(label: String, description: String, symbols: List[Symbol]) object Completion: + def scopeContext(pos: SourcePosition)(using Context): CompletionResult = + val tpdPath = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.span) + val completionContext = Interactive.contextOfPath(tpdPath).withPhase(Phases.typerPhase) + inContext(completionContext): + val untpdPath = Interactive.resolveTypedOrUntypedPath(tpdPath, pos) + val mode = completionMode(untpdPath, pos, forSymbolSearch = true) + val rawPrefix = completionPrefix(untpdPath, pos) + val completer = new Completer(mode, pos, untpdPath, _ => true) + completer.scopeCompletions + /** Get possible completions from tree at `pos` * * @return offset and list of symbols for possible completions @@ -60,7 +70,6 @@ object Completion: val mode = completionMode(untpdPath, pos) val rawPrefix = completionPrefix(untpdPath, pos) val completions = rawCompletions(pos, mode, rawPrefix, tpdPath, untpdPath) - postProcessCompletions(untpdPath, completions, rawPrefix) /** Get possible completions from tree at `pos` @@ -89,7 +98,7 @@ object Completion: * * Otherwise, provide no completion suggestion. */ - def completionMode(path: List[untpd.Tree], pos: SourcePosition): Mode = path match + def completionMode(path: List[untpd.Tree], pos: SourcePosition, forSymbolSearch: Boolean = false): Mode = path match // Ignore `package foo@@` and `package foo.bar@@` case ((_: tpd.Select) | (_: tpd.Ident)):: (_ : tpd.PackageDef) :: _ => Mode.None case GenericImportSelector(sel) => @@ -97,14 +106,19 @@ object Completion: else if sel.isGiven && sel.bound.span.contains(pos.span) then Mode.ImportOrExport else Mode.None // import scala.{util => u@@} case GenericImportOrExport(_) => Mode.ImportOrExport | Mode.Scope // import TrieMa@@ + case untpd.InterpolatedString(_, untpd.Literal(Constants.Constant(_: String)) :: _) :: _ => + Mode.Term | Mode.Scope case untpd.Literal(Constants.Constant(_: String)) :: _ => Mode.Term | Mode.Scope // literal completions case (ref: untpd.RefTree) :: _ => val maybeSelectMembers = if ref.isInstanceOf[untpd.Select] then Mode.Member else Mode.Scope - - if (ref.name.isTermName) Mode.Term | maybeSelectMembers + if (forSymbolSearch) then Mode.Term | Mode.Type | maybeSelectMembers + else if (ref.name.isTermName) Mode.Term | maybeSelectMembers else if (ref.name.isTypeName) Mode.Type | maybeSelectMembers else Mode.None + case (_: tpd.TypeTree | _: tpd.MemberDef) :: _ if forSymbolSearch => Mode.Type | Mode.Term + case (_: tpd.CaseDef) :: _ if forSymbolSearch => Mode.Type | Mode.Term + case Nil if forSymbolSearch => Mode.Type | Mode.Term case _ => Mode.None /** When dealing with in varios palces we check to see if they are @@ -171,6 +185,14 @@ object Completion: case (importOrExport: untpd.ImportOrExport) :: _ => Some(importOrExport) case _ => None + private object StringContextApplication: + def unapply(path: List[tpd.Tree]): Option[tpd.Apply] = + path match + case tpd.Select(qual @ tpd.Apply(tpd.Select(tpd.Select(_, StdNames.nme.StringContext), _), _), _) :: _ => + Some(qual) + case _ => None + + /** Inspect `path` to determine the offset where the completion result should be inserted. */ def completionOffset(untpdPath: List[untpd.Tree]): Int = untpdPath match @@ -220,11 +242,14 @@ object Completion: val result = adjustedPath match // Ignore synthetic select from `This` because in code it was `Ident` // See example in dotty.tools.languageserver.CompletionTest.syntheticThis - case tpd.Select(qual @ tpd.This(_), _) :: _ if qual.span.isSynthetic => completer.scopeCompletions - case tpd.Select(qual, _) :: _ if qual.typeOpt.hasSimpleKind => completer.selectionCompletions(qual) + case tpd.Select(qual @ tpd.This(_), _) :: _ if qual.span.isSynthetic => completer.scopeCompletions.names + case StringContextApplication(qual) => + completer.scopeCompletions.names ++ completer.selectionCompletions(qual) + case tpd.Select(qual, _) :: _ if qual.typeOpt.hasSimpleKind => + completer.selectionCompletions(qual) case tpd.Select(qual, _) :: _ => Map.empty case (tree: tpd.ImportOrExport) :: _ => completer.directMemberCompletions(tree.expr) - case _ => completer.scopeCompletions + case _ => completer.scopeCompletions.names interactiv.println(i"""completion info with pos = $pos, | term = ${completer.mode.is(Mode.Term)}, @@ -325,6 +350,7 @@ object Completion: (completionMode.is(Mode.Term) && (sym.isTerm || sym.is(ModuleClass)) || (completionMode.is(Mode.Type) && (sym.isType || sym.isStableMember))) ) + end isValidCompletionSymbol given ScopeOrdering(using Context): Ordering[Seq[SingleDenotation]] with val order = @@ -358,7 +384,7 @@ object Completion: * (even if the import follows it syntactically) * - a more deeply nested import shadowing a member or a local definition causes an ambiguity */ - def scopeCompletions(using context: Context): CompletionMap = + def scopeCompletions(using context: Context): CompletionResult = /** Temporary data structure representing denotations with the same name introduced in a given scope * as a member of a type, by a local definition or by an import clause @@ -369,14 +395,19 @@ object Completion: ScopedDenotations(denots.filter(includeFn), ctx) val mappings = collection.mutable.Map.empty[Name, List[ScopedDenotations]].withDefaultValue(List.empty) + val renames = collection.mutable.Map.empty[Symbol, Name] def addMapping(name: Name, denots: ScopedDenotations) = mappings(name) = mappings(name) :+ denots ctx.outersIterator.foreach { case ctx @ given Context => if ctx.isImportContext then - importedCompletions.foreach { (name, denots) => + val imported = importedCompletions + imported.names.foreach { (name, denots) => addMapping(name, ScopedDenotations(denots, ctx, include(_, name))) } + imported.renames.foreach { (name, newName) => + renames(name) = newName + } else if ctx.owner.isClass then accessibleMembers(ctx.owner.thisType) .groupByName.foreach { (name, denots) => @@ -420,7 +451,6 @@ object Completion: // most deeply nested member or local definition if not shadowed by an import case Some(local) if local.ctx.scope == first.ctx.scope => resultMappings += name -> local.denots - case None if isSingleImport || isImportedInDifferentScope || isSameSymbolImportedDouble => resultMappings += name -> first.denots case None if notConflictingWithDefaults => @@ -430,7 +460,7 @@ object Completion: } } - resultMappings + CompletionResult(resultMappings, renames.toMap) end scopeCompletions /** Widen only those types which are applied or are exactly nothing @@ -472,15 +502,20 @@ object Completion: /** Completions introduced by imports directly in this context. * Completions from outer contexts are not included. */ - private def importedCompletions(using Context): CompletionMap = + private def importedCompletions(using Context): CompletionResult = val imp = ctx.importInfo + val renames = collection.mutable.Map.empty[Symbol, Name] if imp == null then - Map.empty + CompletionResult(Map.empty, Map.empty) else def fromImport(name: Name, nameInScope: Name): Seq[(Name, SingleDenotation)] = imp.site.member(name).alternatives - .collect { case denot if include(denot, nameInScope) => nameInScope -> denot } + .collect { case denot if include(denot, nameInScope) => + if name != nameInScope then + renames(denot.symbol) = nameInScope + nameInScope -> denot + } val givenImports = imp.importedImplicits .map { ref => (ref.implicitName: Name, ref.underlyingRef.denot.asSingleDenotation) } @@ -506,7 +541,8 @@ object Completion: fromImport(original.toTypeName, nameInScope.toTypeName) }.toSeq.groupByName - givenImports ++ wildcardMembers ++ explicitMembers + val results = givenImports ++ wildcardMembers ++ explicitMembers + CompletionResult(results, renames.toMap) end importedCompletions /** Completions from implicit conversions including old style extensions using implicit classes */ @@ -584,7 +620,7 @@ object Completion: // 1. The extension method is visible under a simple name, by being defined or inherited or imported in a scope enclosing the reference. val termCompleter = new Completer(Mode.Term, pos, untpdPath, matches) - val extMethodsInScope = termCompleter.scopeCompletions.toList.flatMap: + val extMethodsInScope = termCompleter.scopeCompletions.names.toList.flatMap: case (name, denots) => denots.collect: case d: SymDenotation if d.isTerm && d.termRef.symbol.is(Extension) => (d.termRef, name.asTermName) @@ -686,6 +722,7 @@ object Completion: private type CompletionMap = Map[Name, Seq[SingleDenotation]] + case class CompletionResult(names: Map[Name, Seq[SingleDenotation]], renames: Map[Symbol, Name]) /** * The completion mode: defines what kinds of symbols should be included in the completion * results. diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 20eb6e9b33fa..3a43f53f3ca4 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -732,16 +732,17 @@ object Parsers { def testChar(idx: Int, p: Char => Boolean): Boolean = { val txt = source.content - idx < txt.length && p(txt(idx)) + idx >= 0 && idx < txt.length && p(txt(idx)) } def testChar(idx: Int, c: Char): Boolean = { val txt = source.content - idx < txt.length && txt(idx) == c + idx >= 0 && idx < txt.length && txt(idx) == c } def testChars(from: Int, str: String): Boolean = - str.isEmpty || + str.isEmpty + || testChar(from, str.head) && testChars(from + 1, str.tail) def skipBlanks(idx: Int, step: Int = 1): Int = @@ -1600,22 +1601,36 @@ object Parsers { case _ => None } - /** CaptureRef ::= { SimpleRef `.` } SimpleRef [`*`] + /** CaptureRef ::= { SimpleRef `.` } SimpleRef [`*`] [`.` rd] * | [ { SimpleRef `.` } SimpleRef `.` ] id `^` */ def captureRef(): Tree = - val ref = dotSelectors(simpleRef()) - if isIdent(nme.raw.STAR) then - in.nextToken() - atSpan(startOffset(ref)): - PostfixOp(ref, Ident(nme.CC_REACH)) - else if isIdent(nme.UPARROW) then + + def derived(ref: Tree, name: TermName) = in.nextToken() - atSpan(startOffset(ref)): - convertToTypeId(ref) match - case ref: RefTree => makeCapsOf(ref) - case ref => ref - else ref + atSpan(startOffset(ref)) { PostfixOp(ref, Ident(name)) } + + def recur(ref: Tree): Tree = + if in.token == DOT then + in.nextToken() + if in.isIdent(nme.rd) then derived(ref, nme.CC_READONLY) + else recur(selector(ref)) + else if in.isIdent(nme.raw.STAR) then + val reachRef = derived(ref, nme.CC_REACH) + if in.token == DOT && in.lookahead.isIdent(nme.rd) then + in.nextToken() + derived(reachRef, nme.CC_READONLY) + else reachRef + else if isIdent(nme.UPARROW) then + in.nextToken() + atSpan(startOffset(ref)): + convertToTypeId(ref) match + case ref: RefTree => makeCapsOf(ref) + case ref => ref + else ref + + recur(simpleRef()) + end captureRef /** CaptureSet ::= `{` CaptureRef {`,` CaptureRef} `}` -- under captureChecking */ @@ -1624,7 +1639,7 @@ object Parsers { } def capturesAndResult(core: () => Tree): Tree = - if Feature.ccEnabled && in.token == LBRACE && in.offset == in.lastOffset + if Feature.ccEnabled && in.token == LBRACE && canStartCaptureSetContentsTokens.contains(in.lookahead.token) then CapturesAndResult(captureSet(), core()) else core() @@ -1805,8 +1820,9 @@ object Parsers { val start = in.offset val tparams = typeParamClause(ParamOwner.Type) if in.token == TLARROW then + // Filter illegal context bounds and report syntax error atSpan(start, in.skipToken()): - LambdaTypeTree(tparams, toplevelTyp()) + LambdaTypeTree(tparams.mapConserve(stripContextBounds("type lambdas")), toplevelTyp()) else if in.token == ARROW || isPureArrow(nme.PUREARROW) then val arrowOffset = in.skipToken() val body = toplevelTyp(nestedIntoOK(in.token)) @@ -1822,6 +1838,13 @@ object Parsers { typeRest(infixType(inContextBound)) end typ + /** Removes context bounds from TypeDefs and returns a syntax error. */ + private def stripContextBounds(in: String)(tparam: TypeDef) = tparam match + case TypeDef(name, rhs: ContextBounds) => + syntaxError(em"context bounds are not allowed in $in", rhs.span) + TypeDef(name, rhs.bounds) + case other => other + private def makeKindProjectorTypeDef(name: TypeName): TypeDef = { val isVarianceAnnotated = name.startsWith("+") || name.startsWith("-") // We remove the variance marker from the name without passing along the specified variance at all @@ -3304,13 +3327,14 @@ object Parsers { case SEALED => Mod.Sealed() case IDENTIFIER => name match { - case nme.erased if in.erasedEnabled => Mod.Erased() case nme.inline => Mod.Inline() case nme.opaque => Mod.Opaque() case nme.open => Mod.Open() case nme.transparent => Mod.Transparent() case nme.infix => Mod.Infix() case nme.tracked => Mod.Tracked() + case nme.erased if in.erasedEnabled => Mod.Erased() + case nme.mut if Feature.ccEnabled => Mod.Mut() } } @@ -3378,7 +3402,8 @@ object Parsers { * | override * | opaque * LocalModifier ::= abstract | final | sealed | open | implicit | lazy | erased | - * inline | transparent | infix + * inline | transparent | infix | + * mut -- under cc */ def modifiers(allowed: BitSet = modifierTokens, start: Modifiers = Modifiers()): Modifiers = { @tailrec @@ -3482,7 +3507,7 @@ object Parsers { * * HkTypeParamClause ::= ‘[’ HkTypeParam {‘,’ HkTypeParam} ‘]’ * HkTypeParam ::= {Annotation} [‘+’ | ‘-’] - * (id | ‘_’) [HkTypePamClause] TypeBounds + * (id | ‘_’) [HkTypeParamClause] TypeBounds */ def typeParamClause(paramOwner: ParamOwner): List[TypeDef] = inBracketsWithCommas { @@ -3539,7 +3564,7 @@ object Parsers { * ClsParams ::= ClsParam {‘,’ ClsParam} * ClsParam ::= {Annotation} * [{Modifier} (‘val’ | ‘var’)] Param - * TypelessClause ::= DefTermParamClause + * ConstrParamClause ::= DefTermParamClause * | UsingParamClause * * DefTermParamClause::= [nl] ‘(’ [DefTermParams] ‘)’ @@ -3665,7 +3690,7 @@ object Parsers { } /** ClsTermParamClauses ::= {ClsTermParamClause} [[nl] ‘(’ [‘implicit’] ClsParams ‘)’] - * TypelessClauses ::= TypelessClause {TypelessClause} + * ConstrParamClauses ::= ConstrParamClause {ConstrParamClause} * * @return The parameter definitions */ @@ -3939,7 +3964,7 @@ object Parsers { } /** DefDef ::= DefSig [‘:’ Type] [‘=’ Expr] - * | this TypelessClauses [DefImplicitClause] `=' ConstrExpr + * | this ConstrParamClauses [DefImplicitClause] `=' ConstrExpr * DefSig ::= id [DefParamClauses] [DefImplicitClause] */ def defDefOrDcl(start: Offset, mods: Modifiers, numLeadParams: Int = 0): DefDef = atSpan(start, nameStart) { @@ -4691,7 +4716,8 @@ object Parsers { syntaxError(msg, tree.span) Nil tree match - case tree: MemberDef if !(tree.mods.flags & (ModifierFlags &~ Mutable)).isEmpty => + case tree: MemberDef + if !(tree.mods.flags & ModifierFlags).isEmpty && !tree.mods.isMutableVar => // vars are OK, mut defs are not fail(em"refinement cannot be ${(tree.mods.flags & ModifierFlags).flagStrings().mkString("`", "`, `", "`")}") case tree: DefDef if tree.termParamss.nestedExists(!_.rhs.isEmpty) => fail(em"refinement cannot have default arguments") diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 6ae2f7999d33..31f074c3f633 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -1225,7 +1225,10 @@ object Scanners { def isSoftModifier: Boolean = token == IDENTIFIER - && (softModifierNames.contains(name) || name == nme.erased && erasedEnabled || name == nme.tracked && trackedEnabled) + && (softModifierNames.contains(name) + || name == nme.erased && erasedEnabled + || name == nme.tracked && trackedEnabled + || name == nme.mut && Feature.ccEnabled) def isSoftModifierInModifierPosition: Boolean = isSoftModifier && inModifierPosition() diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index c78a336ecdf5..bc55371ec96a 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -295,6 +295,8 @@ object Tokens extends TokensCommon { final val colonEOLPredecessors = BitSet(RPAREN, RBRACKET, BACKQUOTED_IDENT, THIS, SUPER, NEW) + final val canStartCaptureSetContentsTokens = BitSet(IDENTIFIER, BACKQUOTED_IDENT, THIS, RBRACE) + final val closingParens = BitSet(RPAREN, RBRACKET, RBRACE) final val softModifierNames = Set(nme.inline, nme.opaque, nme.open, nme.transparent, nme.infix) diff --git a/compiler/src/dotty/tools/dotc/printing/Formatting.scala b/compiler/src/dotty/tools/dotc/printing/Formatting.scala index ccd7b4e4e282..741b997d9926 100644 --- a/compiler/src/dotty/tools/dotc/printing/Formatting.scala +++ b/compiler/src/dotty/tools/dotc/printing/Formatting.scala @@ -8,7 +8,7 @@ import core.* import Texts.*, Types.*, Flags.*, Symbols.*, Contexts.* import Decorators.* import reporting.Message -import util.DiffUtil +import util.{DiffUtil, SimpleIdentitySet} import Highlighting.* object Formatting { @@ -87,6 +87,9 @@ object Formatting { def show(x: H *: T) = CtxShow(toStr(x.head) *: toShown(x.tail).asInstanceOf[Tuple]) + given [X <: AnyRef: Show]: Show[SimpleIdentitySet[X]] with + def show(x: SimpleIdentitySet[X]) = summon[Show[List[X]]].show(x.toList) + given Show[FlagSet] with def show(x: FlagSet) = x.flagsString diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 53e9a2c2098b..0dcb06ae8c87 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -27,6 +27,12 @@ class PlainPrinter(_ctx: Context) extends Printer { protected def printDebug = ctx.settings.YprintDebug.value + /** Print Fresh instances as */ + protected def ccVerbose = ctx.settings.YccVerbose.value + + /** Print Fresh instances as "fresh" */ + protected def printFresh = ccVerbose || ctx.property(PrintFresh).isDefined + private var openRecs: List[RecType] = Nil protected def maxToTextRecursions: Int = 100 @@ -153,12 +159,14 @@ class PlainPrinter(_ctx: Context) extends Printer { + defn.FromJavaObjectSymbol def toTextCaptureSet(cs: CaptureSet): Text = - if printDebug && ctx.settings.YccDebug.value && !cs.isConst then cs.toString + if printDebug && ctx.settings.YccDebug.value + && !cs.isConst && !cs.isInstanceOf[CaptureSet.HiddenSet] //HiddenSets can be cyclic + then cs.toString else if cs == CaptureSet.Fluid then "" else val core: Text = if !cs.isConst && cs.elems.isEmpty then "?" - else "{" ~ Text(cs.elems.toList.map(toTextCaptureRef), ", ") ~ "}" + else "{" ~ Text(cs.processElems(_.toList.map(toTextCaptureRef)), ", ") ~ "}" // ~ Str("?").provided(!cs.isConst) core ~ cs.optionalInfo @@ -167,8 +175,9 @@ class PlainPrinter(_ctx: Context) extends Printer { toTextCaptureRef(ref.typeOpt) case TypeApply(fn, arg :: Nil) if fn.symbol == defn.Caps_capsOf => toTextRetainedElem(arg) - case _ => - toText(ref) + case ReachCapabilityApply(ref1) => toTextRetainedElem(ref1) ~ "*" + case ReadOnlyCapabilityApply(ref1) => toTextRetainedElem(ref1) ~ ".rd" + case _ => toText(ref) private def toTextRetainedElems[T <: Untyped](refs: List[Tree[T]]): Text = "{" ~ Text(refs.map(ref => toTextRetainedElem(ref)), ", ") ~ "}" @@ -178,8 +187,7 @@ class PlainPrinter(_ctx: Context) extends Printer { */ protected def toTextCapturing(parent: Type, refsText: Text, boxText: Text): Text = changePrec(InfixPrec): - boxText ~ toTextLocal(parent) ~ "^" - ~ (refsText provided refsText != rootSetText) + boxText ~ toTextLocal(parent) ~ "^" ~ (refsText provided refsText != rootSetText) final protected def rootSetText = Str("{cap}") // TODO Use disambiguation @@ -187,7 +195,7 @@ class PlainPrinter(_ctx: Context) extends Printer { homogenize(tp) match { case tp: TypeType => toTextRHS(tp) - case tp: TermRef if tp.isRootCapability => + case tp: TermRef if tp.isCap => toTextCaptureRef(tp) case tp: TermRef if !tp.denotationIsCurrent @@ -203,14 +211,14 @@ class PlainPrinter(_ctx: Context) extends Printer { else toTextPrefixOf(tp) ~ selectionString(tp) case tp: TermParamRef => - ParamRefNameString(tp) ~ lambdaHash(tp.binder) ~ ".type" + ParamRefNameString(tp) ~ hashStr(tp.binder) ~ ".type" case tp: TypeParamRef => val suffix = if showNestingLevel then val tvar = ctx.typerState.constraint.typeVarOfParam(tp) if tvar.exists then s"#${tvar.asInstanceOf[TypeVar].nestingLevel.toString}" else "" else "" - ParamRefNameString(tp) ~ lambdaHash(tp.binder) ~ suffix + ParamRefNameString(tp) ~ hashStr(tp.binder) ~ suffix case tp: SingletonType => toTextSingleton(tp) case AppliedType(tycon, args) => @@ -243,9 +251,32 @@ class PlainPrinter(_ctx: Context) extends Printer { }.close case tp @ CapturingType(parent, refs) => val boxText: Text = Str("box ") provided tp.isBoxed //&& ctx.settings.YccDebug.value - val showAsCap = refs.isUniversal && (refs.elems.size == 1 || !printDebug) - val refsText = if showAsCap then rootSetText else toTextCaptureSet(refs) - toTextCapturing(parent, refsText, boxText) + if parent.derivesFrom(defn.Caps_Capability) + && refs.containsRootCapability && refs.isReadOnly && !printDebug + then + toText(parent) + else + // The set if universal if it consists only of caps.cap or + // only of an existential Fresh that is bound to the immediately enclosing method. + def isUniversal = + refs.elems.size == 1 + && (refs.isUniversal + || !printDebug && !printFresh && !showUniqueIds && refs.elems.nth(0).match + case root.Result(binder) => + CCState.openExistentialScopes match + case b :: _ => binder eq b + case _ => false + case _ => + false + ) + val refsText = + if isUniversal then + rootSetText + else if !refs.elems.isEmpty && refs.elems.forall(_.isCapOrFresh) && !printFresh then + rootSetText + else + toTextCaptureSet(refs) + toTextCapturing(parent, refsText, boxText) case tp @ RetainingType(parent, refs) => if Feature.ccEnabledSomewhere then val refsText = refs match @@ -271,30 +302,30 @@ class PlainPrinter(_ctx: Context) extends Printer { ~ paramsText(tp) ~ ")" ~ (Str(": ") provided !tp.resultType.isInstanceOf[MethodOrPoly]) - ~ toText(tp.resultType) + ~ CCState.inNewExistentialScope(tp)(toText(tp.resultType)) } case ExprType(restp) => def arrowText: Text = restp match case AnnotatedType(parent, ann) if ann.symbol == defn.RetainsByNameAnnot => - val refs = ann.tree.retainedElems - if refs.exists(_.symbol == defn.captureRoot) then Str("=>") - else Str("->") ~ toTextRetainedElems(refs) + ann.tree.retainedElems match + case ref :: Nil if ref.symbol == defn.captureRoot => Str("=>") + case refs => Str("->") ~ toTextRetainedElems(refs) case _ => if Feature.pureFunsEnabled then "->" else "=>" changePrec(GlobalPrec)(arrowText ~ " " ~ toText(restp)) case tp: HKTypeLambda => changePrec(GlobalPrec) { - "[" ~ paramsText(tp) ~ "]" ~ lambdaHash(tp) ~ Str(" =>> ") ~ toTextGlobal(tp.resultType) + "[" ~ paramsText(tp) ~ "]" ~ hashStr(tp) ~ Str(" =>> ") ~ toTextGlobal(tp.resultType) } case tp: PolyType => changePrec(GlobalPrec) { - "[" ~ paramsText(tp) ~ "]" ~ lambdaHash(tp) ~ + "[" ~ paramsText(tp) ~ "]" ~ hashStr(tp) ~ (Str(": ") provided !tp.resultType.isInstanceOf[MethodOrPoly]) ~ toTextGlobal(tp.resultType) } case AnnotatedType(tpe, annot) => - if annot.symbol == defn.InlineParamAnnot || annot.symbol == defn.ErasedParamAnnot - then toText(tpe) + if defn.SilentAnnots.contains(annot.symbol) && !printDebug then + toText(tpe) else if (annot.symbol == defn.IntoAnnot || annot.symbol == defn.IntoParamAnnot) && !printDebug then atPrec(GlobalPrec)( Str("into ") ~ toText(tpe) ) @@ -339,7 +370,7 @@ class PlainPrinter(_ctx: Context) extends Printer { protected def paramsText(lam: LambdaType): Text = { def paramText(ref: ParamRef) = val erased = ref.underlying.hasAnnotation(defn.ErasedParamAnnot) - keywordText("erased ").provided(erased) ~ ParamRefNameString(ref) ~ lambdaHash(lam) ~ toTextRHS(ref.underlying, isParameter = true) + keywordText("erased ").provided(erased) ~ ParamRefNameString(ref) ~ hashStr(lam) ~ toTextRHS(ref.underlying, isParameter = true) Text(lam.paramRefs.map(paramText), ", ") } @@ -351,11 +382,11 @@ class PlainPrinter(_ctx: Context) extends Printer { /** The name of the symbol without a unique id. */ protected def simpleNameString(sym: Symbol): String = nameString(sym.name) - /** If -uniqid is set, the hashcode of the lambda type, after a # */ - protected def lambdaHash(pt: LambdaType): Text = - if (showUniqueIds) - try "#" + pt.hashCode - catch { case ex: NullPointerException => "" } + /** If -uniqid is set, the hashcode of the type, after a # */ + protected def hashStr(tp: Type): String = + if showUniqueIds then + try "#" + tp.hashCode + catch case ex: NullPointerException => "" else "" /** A string to append to a symbol composed of: @@ -404,7 +435,7 @@ class PlainPrinter(_ctx: Context) extends Printer { case tp @ ConstantType(value) => toText(value) case pref: TermParamRef => - ParamRefNameString(pref) ~ lambdaHash(pref.binder) + ParamRefNameString(pref) ~ hashStr(pref.binder) case tp: RecThis => val idx = openRecs.reverse.indexOf(tp.binder) if (idx >= 0) selfRecName(idx + 1) @@ -418,11 +449,25 @@ class PlainPrinter(_ctx: Context) extends Printer { def toTextCaptureRef(tp: Type): Text = homogenize(tp) match - case tp: TermRef if tp.symbol == defn.captureRoot => Str("cap") + case tp: TermRef if tp.symbol == defn.captureRoot => "cap" case tp: SingletonType => toTextRef(tp) case tp: (TypeRef | TypeParamRef) => toText(tp) ~ "^" + case ReadOnlyCapability(tp1) => toTextCaptureRef(tp1) ~ ".rd" case ReachCapability(tp1) => toTextCaptureRef(tp1) ~ "*" case MaybeCapability(tp1) => toTextCaptureRef(tp1) ~ "?" + case tp @ root.Result(binder) => + val idStr = s"##${tp.rootAnnot.id}" + // TODO: Better printing? USe a mode where we print more detailed + val vbleText: Text = CCState.openExistentialScopes.indexOf(binder) match + case -1 => + "" + case n => "outer_" * n ++ (if printFresh then "localcap" else "cap") + vbleText ~ hashStr(binder) ~ Str(idStr).provided(showUniqueIds) + case tp @ root.Fresh(hidden) => + val idStr = if showUniqueIds then s"#${tp.rootAnnot.id}" else "" + if ccVerbose then s"" + else if printFresh then "fresh" + else "cap" case tp => toText(tp) protected def isOmittablePrefix(sym: Symbol): Boolean = @@ -537,7 +582,7 @@ class PlainPrinter(_ctx: Context) extends Printer { else if sym.is(Param) then "parameter" else if sym.is(Given) then "given instance" else if (flags.is(Lazy)) "lazy value" - else if (flags.is(Mutable)) "variable" + else if (sym.isMutableVar) "variable" else if (sym.isClassConstructor && sym.isPrimaryConstructor) "primary constructor" else if (sym.isClassConstructor) "constructor" else if (sym.is(Method)) "method" @@ -553,7 +598,7 @@ class PlainPrinter(_ctx: Context) extends Printer { else if (flags.is(Module)) "object" else if (sym.isClass) "class" else if (sym.isType) "type" - else if (flags.is(Mutable)) "var" + else if (sym.isMutableVarOrAccessor) "var" else if (flags.is(Package)) "package" else if (sym.is(Method)) "def" else if (sym.isTerm && !flags.is(Param)) "val" diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 32115e6bc087..3d987982cc20 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -32,6 +32,7 @@ import dotty.tools.dotc.util.SourcePosition import dotty.tools.dotc.ast.untpd.{MemberDef, Modifiers, PackageDef, RefTree, Template, TypeDef, ValOrDefDef} import cc.* import dotty.tools.dotc.parsing.JavaParsers +import dotty.tools.dotc.transform.TreeExtractors.BinaryOp class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { @@ -161,48 +162,60 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { private def toTextFunction(tp: AppliedType, refs: Text = Str("")): Text = val AppliedType(tycon, args) = (tp: @unchecked) val tsym = tycon.typeSymbol - val isGiven = tsym.name.isContextFunction + val isContextual = tsym.name.isContextFunction val capturesRoot = refs == rootSetText val isPure = Feature.pureFunsEnabled && !tsym.name.isImpureFunction && !capturesRoot - changePrec(GlobalPrec) { - val argStr: Text = - if args.length == 2 - && !defn.isDirectTupleNType(args.head) - && !isGiven - then - atPrec(InfixPrec) { argText(args.head) } - else + toTextFunction(args.init, args.last, tp, refs.provided(!capturesRoot), isContextual, isPure) + + private def toTextFunction(args: List[Type], res: Type, fn: MethodType | AppliedType, refs: Text, + isContextual: Boolean, isPure: Boolean): Text = + changePrec(GlobalPrec): + val argStr: Text = args match + case arg :: Nil if !defn.isDirectTupleNType(arg) && !isContextual => + atPrec(InfixPrec): + argText(arg) + case _=> "(" - ~ argsText(args.init) + ~ argsText(args) ~ ")" - argStr - ~ " " ~ arrow(isGiven, isPure) - ~ (refs provided !capturesRoot) - ~ " " ~ argText(args.last) - } - - protected def toTextMethodAsFunction(info: Type, isPure: Boolean, refs: Text = Str("")): Text = info match - case info: MethodType => - val capturesRoot = refs == rootSetText - changePrec(GlobalPrec) { - "(" - ~ paramsText(info) - ~ ") " - ~ arrow(info.isImplicitMethod, isPure && !capturesRoot) - ~ (refs provided !capturesRoot) - ~ " " - ~ toTextMethodAsFunction(info.resultType, isPure) - } - case info: PolyType => - changePrec(GlobalPrec) { - "[" - ~ paramsText(info) - ~ "] => " - ~ toTextMethodAsFunction(info.resultType, isPure) - } - case _ => - toText(info) + argStr ~ " " ~ arrow(isContextual, isPure) ~ refs ~ " " + ~ fn.match + case fn: MethodType => CCState.inNewExistentialScope(fn)(argText(res)) + case _ => argText(res) + + protected def toTextMethodAsFunction(info: Type, isPure: Boolean, refs: Text = Str("")): Text = + def recur(tp: Type, enclInfo: MethodType | Null): Text = tp match + case tp: MethodType => + val isContextual = tp.isImplicitMethod + val capturesRoot = refs == rootSetText + if cc.isCaptureCheckingOrSetup + && tp.allParamNamesSynthetic + && !tp.looksResultDependent && !tp.looksParamDependent + && !showUniqueIds && !printDebug && !printFresh + then + // cc.Setup converts all functions to dependent functions. Undo that when printing. + toTextFunction(tp.paramInfos, tp.resType, tp, refs.provided(!capturesRoot), isContextual, isPure && !capturesRoot) + else + changePrec(GlobalPrec): + "(" + ~ paramsText(tp) + ~ ") " + ~ arrow(isContextual, isPure && !capturesRoot) + ~ refs.provided(!capturesRoot) + ~ " " + ~ recur(tp.resultType, tp) + case tp: PolyType => + changePrec(GlobalPrec) { + "[" + ~ paramsText(tp) + ~ "] => " + ~ recur(tp.resultType, enclInfo) + } + case _ => + if enclInfo != null then CCState.inNewExistentialScope(enclInfo)(toText(tp)) + else toText(tp) + recur(info, null) override def toText(tp: Type): Text = controlled { def toTextTuple(args: List[Type]): Text = @@ -286,9 +299,6 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { if !printDebug && appliedText(tp.asInstanceOf[HKLambda].resType).isEmpty => // don't eta contract if the application would be printed specially toText(tycon) - case Existential(boundVar, unpacked) - if !printDebug && !ctx.settings.YccDebug.value && !unpacked.existsPart(_ == boundVar) => - toText(unpacked) case tp: RefinedType if defn.isFunctionType(tp) && !printDebug => toTextMethodAsFunction(tp.refinedInfo, isPure = Feature.pureFunsEnabled && !tp.typeSymbol.name.isImpureFunction, @@ -337,7 +347,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { "?" ~ (("(ignored: " ~ toText(ignored) ~ ")") provided printDebug) case tp @ PolyProto(targs, resType) => "[applied to [" ~ toTextGlobal(targs, ", ") ~ "] returning " ~ toText(resType) - case ReachCapability(_) | MaybeCapability(_) => + case tp: AnnotatedType if tp.isTrackableRef => toTextCaptureRef(tp) case _ => super.toText(tp) @@ -379,6 +389,10 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { def optDotPrefix(tree: This) = optText(tree.qual)(_ ~ ".") provided !isLocalThis(tree) + /** Should a binary operation with this operator be printed infix? */ + def isInfix(op: Symbol) = + op.exists && (op.isDeclaredInfix || op.name.isOperatorName) + def caseBlockText(tree: Tree): Text = tree match { case Block(stats, expr) => toText(stats :+ expr, "\n") case expr => toText(expr) @@ -470,6 +484,13 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { optDotPrefix(tree) ~ keywordStr("this") ~ idText(tree) case Super(qual: This, mix) => optDotPrefix(qual) ~ keywordStr("super") ~ optText(mix)("[" ~ _ ~ "]") + case BinaryOp(l, op, r) if isInfix(op) => + val isRightAssoc = op.name.endsWith(":") + val opPrec = parsing.precedence(op.name) + val leftPrec = if isRightAssoc then opPrec + 1 else opPrec + val rightPrec = if !isRightAssoc then opPrec + 1 else opPrec + changePrec(opPrec): + atPrec(leftPrec)(toText(l)) ~ " " ~ toText(op.name) ~ " " ~ atPrec(rightPrec)(toText(r)) case app @ Apply(fun, args) => if (fun.hasType && fun.symbol == defn.throwMethod) changePrec (GlobalPrec) { @@ -496,7 +517,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { } } case Typed(expr, tpt) => - changePrec(InfixPrec) { + changePrec(DotPrec) { if isWildcardStarArg(tree) then expr match case Ident(nme.WILDCARD_STAR) => @@ -744,6 +765,8 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { case PostfixOp(l, op) => if op.name == nme.CC_REACH then changePrec(DotPrec) { toText(l) ~ "*" } + else if op.name == nme.CC_READONLY then + changePrec(DotPrec) { toText(l) ~ ".rd" } else changePrec(InfixPrec) { toText(l) ~ " " ~ toText(op) } case PrefixOp(op, r) => @@ -938,7 +961,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { tree.hasType && tree.symbol.exists && ctx.settings.YprintSyms.value protected def nameIdText[T <: Untyped](tree: NameTree[T]): Text = - if (tree.hasType && tree.symbol.exists) { + if (tree.hasType && tree.symbol.exists && tree.symbol.isType == tree.name.isTypeName) { val str = nameString(tree.symbol) tree match { case tree: RefTree => withPos(str, tree.sourcePos) diff --git a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala index 8f8f4676f43b..cafe99973701 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -222,6 +222,9 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe case EnumMayNotBeValueClassesID // errorNumber: 206 case IllegalUnrollPlacementID // errorNumber: 207 case ExtensionHasDefaultID // errorNumber: 208 + case FormatInterpolationErrorID // errorNumber: 209 + case ValueClassCannotExtendAliasOfAnyValID // errorNumber: 210 + case MatchIsNotPartialFunctionID // errorNumber: 211 def errorNumber = ordinal - 1 diff --git a/compiler/src/dotty/tools/dotc/reporting/MessageKind.scala b/compiler/src/dotty/tools/dotc/reporting/MessageKind.scala index bb02a08d2e46..e09dd1d6e69e 100644 --- a/compiler/src/dotty/tools/dotc/reporting/MessageKind.scala +++ b/compiler/src/dotty/tools/dotc/reporting/MessageKind.scala @@ -23,6 +23,7 @@ enum MessageKind: case PotentialIssue case UnusedSymbol case Staging + case Interpolation /** Human readable message that will end up being shown to the user. * NOTE: This is only used in the situation where you have multiple words diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index b5d67f0808b2..d4b7e2901568 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -1694,7 +1694,7 @@ class OnlyClassesCanHaveDeclaredButUndefinedMembers(sym: Symbol)( def msg(using Context) = i"""Declaration of $sym not allowed here: only classes can have declared but undefined members""" def explain(using Context) = - if sym.is(Mutable) then "Note that variables need to be initialized to be defined." + if sym.isMutableVarOrAccessor then "Note that variables need to be initialized to be defined." else "" } @@ -1813,6 +1813,12 @@ class ValueClassParameterMayNotBeCallByName(valueClass: Symbol, param: Symbol)(u def explain(using Context) = "" } +class ValueClassCannotExtendAliasOfAnyVal(valueClass: Symbol, alias: Symbol)(using Context) + extends SyntaxMsg(ValueClassCannotExtendAliasOfAnyValID) { + def msg(using Context) = i"""A value class cannot extend a type alias ($alias) of ${hl("AnyVal")}""" + def explain(using Context) = "" +} + class SuperCallsNotAllowedInlineable(symbol: Symbol)(using Context) extends SyntaxMsg(SuperCallsNotAllowedInlineableID) { def msg(using Context) = i"Super call not allowed in inlineable $symbol" @@ -2421,13 +2427,15 @@ class ClassCannotExtendEnum(cls: Symbol, parent: Symbol)(using Context) extends } class NotAnExtractor(tree: untpd.Tree)(using Context) extends PatternMatchMsg(NotAnExtractorID) { - def msg(using Context) = i"$tree cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method" + def msg(using Context) = i"$tree cannot be used as an extractor in a pattern because it lacks an ${hl("unapply")} or ${hl("unapplySeq")} method with the appropriate signature" def explain(using Context) = - i"""|An ${hl("unapply")} method should be defined in an ${hl("object")} as follow: + i"""|An ${hl("unapply")} method should be in an ${hl("object")}, take a single explicit term parameter, and: | - If it is just a test, return a ${hl("Boolean")}. For example ${hl("case even()")} | - If it returns a single sub-value of type T, return an ${hl("Option[T]")} | - If it returns several sub-values T1,...,Tn, group them in an optional tuple ${hl("Option[(T1,...,Tn)]")} | + |Additionaly, ${hl("unapply")} or ${hl("unapplySeq")} methods cannot take type parameters after their explicit term parameter. + | |Sometimes, the number of sub-values isn't fixed and we would like to return a sequence. |For this reason, you can also define patterns through ${hl("unapplySeq")} which returns ${hl("Option[Seq[T]]")}. |This mechanism is used for instance in pattern ${hl("case List(x1, ..., xn)")}""" @@ -3302,23 +3310,29 @@ extends TypeMsg(ConstructorProxyNotValueID): |are not values themselves, they can only be referred to in selections.""" class UnusedSymbol(errorText: String, val actions: List[CodeAction] = Nil)(using Context) -extends Message(UnusedSymbolID) { +extends Message(UnusedSymbolID): def kind = MessageKind.UnusedSymbol override def msg(using Context) = errorText override def explain(using Context) = "" override def actions(using Context) = this.actions -} object UnusedSymbol: def imports(actions: List[CodeAction])(using Context): UnusedSymbol = UnusedSymbol(i"unused import", actions) def localDefs(using Context): UnusedSymbol = UnusedSymbol(i"unused local definition") - def explicitParams(using Context): UnusedSymbol = UnusedSymbol(i"unused explicit parameter") - def implicitParams(using Context): UnusedSymbol = UnusedSymbol(i"unused implicit parameter") + def explicitParams(sym: Symbol)(using Context): UnusedSymbol = + UnusedSymbol(i"unused explicit parameter${paramAddendum(sym)}") + def implicitParams(sym: Symbol)(using Context): UnusedSymbol = + UnusedSymbol(i"unused implicit parameter${paramAddendum(sym)}") def privateMembers(using Context): UnusedSymbol = UnusedSymbol(i"unused private member") def patVars(using Context): UnusedSymbol = UnusedSymbol(i"unused pattern variable") - def unsetLocals(using Context): UnusedSymbol = UnusedSymbol(i"unset local variable, consider using an immutable val instead") - def unsetPrivates(using Context): UnusedSymbol = UnusedSymbol(i"unset private variable, consider using an immutable val instead") + def unsetLocals(using Context): UnusedSymbol = + UnusedSymbol(i"unset local variable, consider using an immutable val instead") + def unsetPrivates(using Context): UnusedSymbol = + UnusedSymbol(i"unset private variable, consider using an immutable val instead") + private def paramAddendum(sym: Symbol)(using Context): String = + if sym.denot.owner.is(ExtensionMethod) then i" in extension ${sym.denot.owner}" + else "" class NonNamedArgumentInJavaAnnotation(using Context) extends SyntaxMsg(NonNamedArgumentInJavaAnnotationID): @@ -3430,12 +3444,12 @@ extends DeclarationMsg(IllegalUnrollPlacementID): val isCtor = method.isConstructor def what = if isCtor then i"a ${if method.owner.is(Trait) then "trait" else "class"} constructor" else i"method ${method.name}" val prefix = s"Cannot unroll parameters of $what" - if method.is(Deferred) then - i"$prefix: it must not be abstract" + if method.isLocal then + i"$prefix because it is a local method" + else if !method.isEffectivelyFinal then + i"$prefix because it can be overridden" else if isCtor && method.owner.is(Trait) then i"implementation restriction: $prefix" - else if !(isCtor || method.is(Final) || method.owner.is(ModuleClass)) then - i"$prefix: it is not final" else if method.owner.companionClass.is(CaseClass) then i"$prefix of a case class companion object: please annotate the class constructor instead" else @@ -3444,3 +3458,38 @@ extends DeclarationMsg(IllegalUnrollPlacementID): def explain(using Context) = "" end IllegalUnrollPlacement + +class BadFormatInterpolation(errorText: String)(using Context) extends Message(FormatInterpolationErrorID): + def kind = MessageKind.Interpolation + protected def msg(using Context) = errorText + protected def explain(using Context) = "" + +class MatchIsNotPartialFunction(using Context) extends SyntaxMsg(MatchIsNotPartialFunctionID): + protected def msg(using Context) = + "match expression in result of block will not be used to synthesize partial function" + protected def explain(using Context) = + i"""A `PartialFunction` can be synthesized from a function literal if its body is just a pattern match. + | + |For example, `collect` takes a `PartialFunction`. + | (1 to 10).collect(i => i match { case n if n % 2 == 0 => n }) + |is equivalent to using a "pattern-matching anonymous function" directly: + | (1 to 10).collect { case n if n % 2 == 0 => n } + |Compare an operation that requires a `Function1` instead: + | (1 to 10).map { case n if n % 2 == 0 => n case n => n + 1 } + | + |As a convenience, the "selector expression" of the match can be an arbitrary expression: + | List("1", "two", "3").collect(x => Try(x.toInt) match { case Success(i) => i }) + |In this example, `isDefinedAt` evaluates the selector expression and any guard expressions + |in the pattern match in order to report whether an input is in the domain of the function. + | + |However, blocks of statements are not supported by this idiom: + | List("1", "two", "3").collect: x => + | val maybe = Try(x.toInt) // statements preceding the match + | maybe match + | case Success(i) if i % 2 == 0 => i // throws MatchError on cases not covered + | + |This restriction is enforced to simplify the evaluation semantics of the partial function. + |Otherwise, it might not be clear what is computed by `isDefinedAt`. + | + |Efficient operations will use `applyOrElse` to avoid computing the match twice, + |but the `apply` body would be executed "per element" in the example.""" diff --git a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala index c303c40485ce..4d915b57df1b 100644 --- a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala +++ b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala @@ -418,7 +418,7 @@ private class ExtractAPICollector(nonLocalClassSymbols: mutable.HashSet[Symbol]) apiClass(sym.asClass) } else if (sym.isType) { apiTypeMember(sym.asType) - } else if (sym.is(Mutable, butNot = Accessor)) { + } else if (sym.isMutableVar) { api.Var.of(sym.name.toString, apiAccess(sym), apiModifiers(sym), apiAnnotations(sym, inlineOrigin).toArray, apiType(sym.info)) } else if (sym.isStableMember && !sym.isRealMethod) { diff --git a/compiler/src/dotty/tools/dotc/transform/CapturedVars.scala b/compiler/src/dotty/tools/dotc/transform/CapturedVars.scala index c1725cbd0255..7263bce0478c 100644 --- a/compiler/src/dotty/tools/dotc/transform/CapturedVars.scala +++ b/compiler/src/dotty/tools/dotc/transform/CapturedVars.scala @@ -120,7 +120,7 @@ object CapturedVars: def traverse(tree: Tree)(using Context) = tree match case id: Ident => val sym = id.symbol - if sym.is(Mutable, butNot = Method) && sym.owner.isTerm then + if sym.isMutableVar && sym.owner.isTerm then val enclMeth = ctx.owner.enclosingMethod if sym.enclosingMethod != enclMeth then report.log(i"capturing $sym in ${sym.enclosingMethod}, referenced from $enclMeth") diff --git a/compiler/src/dotty/tools/dotc/transform/CheckReentrant.scala b/compiler/src/dotty/tools/dotc/transform/CheckReentrant.scala index e8a402068bfc..5f52ac82879a 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckReentrant.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckReentrant.scala @@ -65,7 +65,7 @@ class CheckReentrant extends MiniPhase { scanning(cls) { for (sym <- cls.classInfo.decls) if (sym.isTerm && !sym.isSetter && !isIgnored(sym)) - if (sym.is(Mutable)) { + if (sym.isMutableVarOrAccessor) { report.error( em"""possible data race involving globally reachable ${sym.showLocated}: ${sym.info} | use -Ylog:checkReentrant+ to find out more about why the variable is reachable.""") diff --git a/compiler/src/dotty/tools/dotc/transform/CheckShadowing.scala b/compiler/src/dotty/tools/dotc/transform/CheckShadowing.scala index 3adb3ab0ce7d..e6fe64fe7b62 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckShadowing.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckShadowing.scala @@ -18,7 +18,6 @@ import dotty.tools.dotc.core.Types.NoType import dotty.tools.dotc.core.Types.Type import dotty.tools.dotc.core.Types import dotty.tools.dotc.semanticdb.TypeOps -import dotty.tools.dotc.cc.boxedCaptureSet import dotty.tools.dotc.core.Symbols.{NoSymbol, isParamOrAccessor} import scala.collection.mutable import dotty.tools.dotc.core.Scopes.Scope diff --git a/compiler/src/dotty/tools/dotc/transform/CheckStatic.scala b/compiler/src/dotty/tools/dotc/transform/CheckStatic.scala index 6c74f302b65d..957fd78e9c2c 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckStatic.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckStatic.scala @@ -52,7 +52,7 @@ class CheckStatic extends MiniPhase { report.error(MissingCompanionForStatic(defn.symbol), defn.srcPos) else if (clashes.exists) report.error(MemberWithSameNameAsStatic(), defn.srcPos) - else if (defn.symbol.is(Flags.Mutable) && companion.is(Flags.Trait)) + else if (defn.symbol.isMutableVarOrAccessor && companion.is(Flags.Trait)) report.error(TraitCompanionWithMutableStatic(), defn.srcPos) else if (defn.symbol.is(Flags.Lazy)) report.error(LazyStaticField(), defn.srcPos) diff --git a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala index 2ee3ea825edc..6b5dd96f0493 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala @@ -154,6 +154,19 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha override def prepareForBind(tree: Bind)(using Context): Context = refInfos.register(tree) ctx + /* cf QuotePattern + override def transformBind(tree: Bind)(using Context): tree.type = + tree.symbol.info match + case TypeBounds(lo, hi) => + def resolve(tpe: Type): Unit = + val sym = tpe.typeSymbol + if sym.exists then + resolveUsage(sym, sym.name, NoPrefix) + resolve(lo) + resolve(hi) + case _ => + tree + */ override def prepareForValDef(tree: ValDef)(using Context): Context = if !tree.symbol.is(Deferred) && tree.rhs.symbol != defn.Predef_undefined then @@ -202,15 +215,6 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha refInfos.register(tree) tree - override def prepareForTemplate(tree: Template)(using Context): Context = - ctx.fresh.setProperty(resolvedKey, Resolved()) - - override def prepareForPackageDef(tree: PackageDef)(using Context): Context = - ctx.fresh.setProperty(resolvedKey, Resolved()) - - override def prepareForStats(trees: List[Tree])(using Context): Context = - ctx.fresh.setProperty(resolvedKey, Resolved()) - override def transformOther(tree: Tree)(using Context): tree.type = tree match case imp: Import => @@ -222,6 +226,8 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha selector.bound match case untpd.TypedSplice(bound) => transformAllDeep(bound) case _ => + case exp: Export => + transformAllDeep(exp.expr) case AppliedTypeTree(tpt, args) => transformAllDeep(tpt) args.foreach(transformAllDeep) @@ -250,7 +256,17 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha case Splice(expr) => transformAllDeep(expr) case QuotePattern(bindings, body, quotes) => - bindings.foreach(transformAllDeep) + bindings.foreach: + case b @ Bind(_, _) => + b.symbol.info match + case TypeBounds(lo, hi) => + def resolve(tpe: Type): Unit = + val sym = tpe.typeSymbol + if sym.exists then + resolveUsage(sym, sym.name, NoPrefix) + resolve(lo) + resolve(hi) + case _ => transformAllDeep(body) transformAllDeep(quotes) case SplicePattern(body, typeargs, args) => @@ -264,7 +280,6 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha case ByNameTypeTree(result) => transformAllDeep(result) //case _: InferredTypeTree => // do nothing - //case _: Export => // nothing to do //case _ if tree.isType => case _ => tree @@ -292,7 +307,9 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha def matchingSelector(info: ImportInfo): ImportSelector | Null = val qtpe = info.site - def hasAltMember(nm: Name) = qtpe.member(nm).hasAltWith(_.symbol == sym) + def hasAltMember(nm: Name) = qtpe.member(nm).hasAltWith: alt => + alt.symbol == sym + || nm.isTypeName && alt.symbol.isAliasType && alt.info.dealias.typeSymbol == sym def loop(sels: List[ImportSelector]): ImportSelector | Null = sels match case sel :: sels => val matches = @@ -323,15 +340,6 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha && ctxsym.thisType.baseClasses.contains(sym.owner) && ctxsym.thisType.member(sym.name).hasAltWith(d => d.containsSym(sym) && !name.exists(_ != d.name)) - // Attempt to cache a result at the given context. Not all contexts bear a cache, including NoContext. - // If there is already any result for the name and prefix, do nothing. - def addCached(where: Context, result: Precedence): Unit = - if where.moreProperties ne null then - where.property(resolvedKey) match - case Some(resolved) => - resolved.record(sym, name, prefix, result) - case none => - // Avoid spurious NoSymbol and also primary ctors which are never warned about. // Selections C.this.toString should be already excluded, but backtopped here for eq, etc. if !sym.exists || sym.isPrimaryConstructor || sym.isEffectiveRoot || defn.topClasses(sym.owner) then return @@ -340,39 +348,20 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha // If the sym is an enclosing definition (the owner of a context), it does not count toward usages. val isLocal = sym.isLocalToBlock var candidate: Context = NoContext - var cachePoint: Context = NoContext // last context with Resolved cache var importer: ImportSelector | Null = null // non-null for import context var precedence = NoPrecedence // of current resolution + var enclosed = false // true if sym is owner of an enclosing context var done = false - var cached = false val ctxs = ctx.outersIterator while !done && ctxs.hasNext do val cur = ctxs.next() - if cur.owner eq sym then - addCached(cachePoint, Definition) - return // found enclosing definition - else if isLocal then + if cur.owner.userSymbol == sym && !sym.is(Package) then + enclosed = true // found enclosing definition, don't register the reference + if isLocal then if cur.owner eq sym.owner then done = true // for local def, just checking that it is not enclosing else - val cachedPrecedence = - cur.property(resolvedKey) match - case Some(resolved) => - // conservative, cache must be nested below the result context - if precedence.isNone then - cachePoint = cur // no result yet, and future result could be cached here - resolved.hasRecord(sym, name, prefix) - case none => NoPrecedence - cached = !cachedPrecedence.isNone - if cached then - // if prefer cached precedence, then discard previous result - if precedence.weakerThan(cachedPrecedence) then - candidate = NoContext - importer = null - cachePoint = cur // actual cache context - precedence = cachedPrecedence // actual cached precedence - done = true - else if cur.isImportContext then + if cur.isImportContext then val sel = matchingSelector(cur.importInfo.nn) if sel != null then if cur.importInfo.nn.isRootImport then @@ -392,7 +381,7 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha candidate = cur importer = sel else if checkMember(cur.owner) then - if sym.srcPos.sourcePos.source == ctx.source then + if sym.is(Package) || sym.srcPos.sourcePos.source == ctx.source then precedence = Definition candidate = cur importer = null // ignore import in same scope; we can't check nesting level @@ -402,16 +391,10 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha candidate = cur end while // record usage and possibly an import - refInfos.refs.addOne(sym) + if !enclosed then + refInfos.refs.addOne(sym) if candidate != NoContext && candidate.isImportContext && importer != null then refInfos.sels.put(importer, ()) - // possibly record that we have performed this look-up - // if no result was found, take it as Definition (local or rooted head of fully qualified path) - val adjusted = if precedence.isNone then Definition else precedence - if !cached && (cachePoint ne NoContext) then - addCached(cachePoint, adjusted) - if cachePoint ne ctx then - addCached(ctx, adjusted) // at this ctx, since cachePoint may be far up the outer chain end resolveUsage end CheckUnused @@ -423,15 +406,8 @@ object CheckUnused: val refInfosKey = Property.StickyKey[RefInfos] - val resolvedKey = Property.Key[Resolved] - inline def refInfos(using Context): RefInfos = ctx.property(refInfosKey).get - inline def resolved(using Context): Resolved = - ctx.property(resolvedKey) match - case Some(res) => res - case _ => throw new MatchError("no Resolved for context") - /** Attachment holding the name of an Ident as written by the user. */ val OriginalName = Property.StickyKey[Name] @@ -460,7 +436,7 @@ object CheckUnused: if inliners == 0 && languageImport(imp.expr).isEmpty && !imp.isGeneratedByEnum - && !ctx.outer.owner.name.isReplWrapperName + && !ctx.owner.name.isReplWrapperName then imps.put(imp, ()) case tree: Bind => @@ -487,24 +463,6 @@ object CheckUnused: var inliners = 0 // depth of inline def (not inlined yet) end RefInfos - // Symbols already resolved in the given Context (with name and prefix of lookup). - class Resolved: - import PrecedenceLevels.* - private val seen = mutable.Map.empty[Symbol, List[(Name, Type, Precedence)]].withDefaultValue(Nil) - // if a result has been recorded, return it; otherwise, NoPrecedence. - def hasRecord(symbol: Symbol, name: Name, prefix: Type)(using Context): Precedence = - seen(symbol).find((n, p, _) => n == name && p =:= prefix) match - case Some((_, _, r)) => r - case none => NoPrecedence - // "record" the look-up result, if there is not already a result for the name and prefix. - def record(symbol: Symbol, name: Name, prefix: Type, result: Precedence)(using Context): Unit = - require(NoPrecedence.weakerThan(result)) - seen.updateWith(symbol): - case svs @ Some(vs) => - if vs.exists((n, p, _) => n == name && p =:= prefix) then svs - else Some((name, prefix, result) :: vs) - case none => Some((name, prefix, result) :: Nil) - // Names are resolved by definitions and imports, which have four precedence levels: object PrecedenceLevels: opaque type Precedence = Int @@ -530,6 +488,8 @@ object CheckUnused: val warnings = ArrayBuilder.make[MessageInfo] def warnAt(pos: SrcPos)(msg: UnusedSymbol, origin: String = ""): Unit = warnings.addOne((msg, pos, origin)) val infos = refInfos + //println(infos.defs.mkString("DEFS\n", "\n", "\n---")) + //println(infos.refs.mkString("REFS\n", "\n", "\n---")) def checkUnassigned(sym: Symbol, pos: SrcPos) = if sym.isLocalToBlock then @@ -570,7 +530,7 @@ object CheckUnused: if aliasSym.isAllOf(PrivateParamAccessor, butNot = CaseAccessor) && !infos.refs(alias.symbol) then if aliasSym.is(Local) then if ctx.settings.WunusedHas.explicits then - warnAt(pos)(UnusedSymbol.explicitParams) + warnAt(pos)(UnusedSymbol.explicitParams(aliasSym)) else if ctx.settings.WunusedHas.privates then warnAt(pos)(UnusedSymbol.privateMembers) @@ -584,11 +544,11 @@ object CheckUnused: && !sym.name.isInstanceOf[DerivedName] && !ctx.platform.isMainMethod(m) then - warnAt(pos)(UnusedSymbol.explicitParams) + warnAt(pos)(UnusedSymbol.explicitParams(sym)) end checkExplicit // begin if !infos.skip(m) - && !m.nextOverriddenSymbol.exists + && !m.isEffectivelyOverride && !allowed then checkExplicit() @@ -600,18 +560,20 @@ object CheckUnused: val dd = defn m.isDeprecated || m.is(Synthetic) - || sym.name.is(ContextFunctionParamName) // a ubiquitous parameter - || sym.name.is(ContextBoundParamName) && sym.info.typeSymbol.isMarkerTrait // a ubiquitous parameter || m.hasAnnotation(dd.UnusedAnnot) // param of unused method + || sym.name.is(ContextFunctionParamName) // a ubiquitous parameter + || sym.isCanEqual || sym.info.typeSymbol.match // more ubiquity case dd.DummyImplicitClass | dd.SubTypeClass | dd.SameTypeClass => true - case _ => false + case tps => + tps.isMarkerTrait // no members to use; was only if sym.name.is(ContextBoundParamName) + || // but consider NotGiven + tps.hasAnnotation(dd.LanguageFeatureMetaAnnot) || sym.info.isSingleton // DSL friendly - || sym.isCanEqual - || sym.info.typeSymbol.hasAnnotation(dd.LanguageFeatureMetaAnnot) || sym.info.isInstanceOf[RefinedType] // can't be expressed as a context bound if ctx.settings.WunusedHas.implicits && !infos.skip(m) + && !m.isEffectivelyOverride && !allowed then if m.isPrimaryConstructor then @@ -622,9 +584,9 @@ object CheckUnused: aliasSym.isAllOf(PrivateParamAccessor, butNot = CaseAccessor) || aliasSym.isAllOf(Protected | ParamAccessor, butNot = CaseAccessor) && m.owner.is(Given) if checking && !infos.refs(alias.symbol) then - warnAt(pos)(UnusedSymbol.implicitParams) + warnAt(pos)(UnusedSymbol.implicitParams(aliasSym)) else - warnAt(pos)(UnusedSymbol.implicitParams) + warnAt(pos)(UnusedSymbol.implicitParams(sym)) def checkLocal(sym: Symbol, pos: SrcPos) = if ctx.settings.WunusedHas.locals @@ -889,41 +851,49 @@ object CheckUnused: inline def exists(p: Name => Boolean): Boolean = nm.ne(nme.NO_NAME) && p(nm) inline def isWildcard: Boolean = nm == nme.WILDCARD || nm.is(WildcardParamName) - extension (tp: Type) - def importPrefix(using Context): Type = tp match + extension (tp: Type)(using Context) + def importPrefix: Type = tp match case tp: NamedType => tp.prefix case tp: ClassInfo => tp.prefix case tp: TypeProxy => tp.superType.normalizedPrefix case _ => NoType - def underlyingPrefix(using Context): Type = tp match + def underlyingPrefix: Type = tp match case tp: NamedType => tp.prefix case tp: ClassInfo => tp.prefix case tp: TypeProxy => tp.underlying.underlyingPrefix case _ => NoType - def skipPackageObject(using Context): Type = + def skipPackageObject: Type = if tp.typeSymbol.isPackageObject then tp.underlyingPrefix else tp - def underlying(using Context): Type = tp match + def underlying: Type = tp match case tp: TypeProxy => tp.underlying case _ => tp private val serializationNames: Set[TermName] = Set("readResolve", "readObject", "readObjectNoData", "writeObject", "writeReplace").map(termName(_)) - extension (sym: Symbol) - def isSerializationSupport(using Context): Boolean = + extension (sym: Symbol)(using Context) + def isSerializationSupport: Boolean = sym.is(Method) && serializationNames(sym.name.toTermName) && sym.owner.isClass && sym.owner.derivesFrom(defn.JavaSerializableClass) - def isCanEqual(using Context): Boolean = + def isCanEqual: Boolean = sym.isOneOf(GivenOrImplicit) && sym.info.finalResultType.baseClasses.exists(_.derivesFrom(defn.CanEqualClass)) - def isMarkerTrait(using Context): Boolean = + def isMarkerTrait: Boolean = sym.isClass && sym.info.allMembers.forall: d => val m = d.symbol !m.isTerm || m.isSelfSym || m.is(Method) && (m.owner == defn.AnyClass || m.owner == defn.ObjectClass) - def isEffectivelyPrivate(using Context): Boolean = + def isEffectivelyPrivate: Boolean = sym.is(Private, butNot = ParamAccessor) - || sym.owner.isAnonymousClass && !sym.nextOverriddenSymbol.exists + || sym.owner.isAnonymousClass && !sym.isEffectivelyOverride + def isEffectivelyOverride: Boolean = + sym.is(Override) + || + sym.canMatchInheritedSymbols && { // inline allOverriddenSymbols using owner.info or thisType + val owner = sym.owner.asClass + val base = if owner.classInfo.selfInfo != NoType then owner.thisType else owner.info + base.baseClasses.drop(1).iterator.exists(sym.overriddenSymbol(_).exists) + } // pick the symbol the user wrote for purposes of tracking - inline def userSymbol(using Context): Symbol= + inline def userSymbol: Symbol= if sym.denot.is(ModuleClass) then sym.denot.companionModule else sym extension (sel: ImportSelector) @@ -937,13 +907,13 @@ object CheckUnused: case untpd.Ident(nme.WILDCARD) => true case _ => false - extension (imp: Import) + extension (imp: Import)(using Context) /** Is it the first import clause in a statement? `a.x` in `import a.x, b.{y, z}` */ - def isPrimaryClause(using Context): Boolean = + def isPrimaryClause: Boolean = imp.srcPos.span.pointDelta > 0 // primary clause starts at `import` keyword with point at clause proper /** Generated import of cases from enum companion. */ - def isGeneratedByEnum(using Context): Boolean = + def isGeneratedByEnum: Boolean = imp.symbol.exists && imp.symbol.owner.is(Enum, butNot = Case) /** Under -Wunused:strict-no-implicit-warn, avoid false positives @@ -951,7 +921,7 @@ object CheckUnused: * specifically does import an implicit. * Similarly, import of CanEqual must not warn, as it is always witness. */ - def isLoose(sel: ImportSelector)(using Context): Boolean = + def isLoose(sel: ImportSelector): Boolean = if ctx.settings.WunusedHas.strictNoImplicitWarn then if sel.isWildcard || imp.expr.tpe.member(sel.name.toTermName).hasAltWith(_.symbol.isOneOf(GivenOrImplicit)) diff --git a/compiler/src/dotty/tools/dotc/transform/Constructors.scala b/compiler/src/dotty/tools/dotc/transform/Constructors.scala index 9a0df830c6d7..b373565489f0 100644 --- a/compiler/src/dotty/tools/dotc/transform/Constructors.scala +++ b/compiler/src/dotty/tools/dotc/transform/Constructors.scala @@ -155,7 +155,7 @@ class Constructors extends MiniPhase with IdentityDenotTransformer { thisPhase = case Ident(_) | Select(This(_), _) => var sym = tree.symbol def isOverridableSelect = tree.isInstanceOf[Select] && !sym.isEffectivelyFinal - def switchOutsideSupercall = !sym.is(Mutable) && !isOverridableSelect + def switchOutsideSupercall = !sym.isMutableVarOrAccessor && !isOverridableSelect // If true, switch to constructor parameters also in the constructor body // that follows the super call. // Variables need to go through the getter since they might have been updated. diff --git a/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala b/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala index 67bf1bebed87..68f911f06963 100644 --- a/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala +++ b/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala @@ -126,10 +126,16 @@ class ExpandSAMs extends MiniPhase: // The right hand side from which to construct the partial function. This is always a Match. // If the original rhs is already a Match (possibly in braces), return that. // Otherwise construct a match `x match case _ => rhs` where `x` is the parameter of the closure. - def partialFunRHS(tree: Tree): Match = tree match + def partialFunRHS(tree: Tree): Match = + inline def checkMatch(): Unit = + tree match + case Block(_, m: Match) => report.warning(reporting.MatchIsNotPartialFunction(), m.srcPos) + case _ => + tree match case m: Match => m case Block(Nil, expr) => partialFunRHS(expr) case _ => + checkMatch() Match(ref(param.symbol), CaseDef(untpd.Ident(nme.WILDCARD).withType(param.symbol.info), EmptyTree, tree) :: Nil) diff --git a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala index e2712a7d6302..2fd777f715d9 100644 --- a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala +++ b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala @@ -255,7 +255,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { def transformMemberDefThreadUnsafe(x: ValOrDefDef)(using Context): Thicket = { val claz = x.symbol.owner.asClass val tpe = x.tpe.widen.resultType.widen - assert(!(x.symbol is Mutable)) + assert(!x.symbol.isMutableVarOrAccessor) val containerName = LazyLocalName.fresh(x.name.asTermName) val containerSymbol = newSymbol(claz, containerName, x.symbol.flags &~ containerFlagsMask | containerFlags | Private, @@ -447,7 +447,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { } def transformMemberDefThreadSafe(x: ValOrDefDef)(using Context): Thicket = { - assert(!(x.symbol is Mutable)) + assert(!x.symbol.isMutableVarOrAccessor) if ctx.settings.YlegacyLazyVals.value then transformMemberDefThreadSafeLegacy(x) else diff --git a/compiler/src/dotty/tools/dotc/transform/MoveStatics.scala b/compiler/src/dotty/tools/dotc/transform/MoveStatics.scala index 95975ad9e6b8..b3ec05501b5b 100644 --- a/compiler/src/dotty/tools/dotc/transform/MoveStatics.scala +++ b/compiler/src/dotty/tools/dotc/transform/MoveStatics.scala @@ -28,7 +28,7 @@ class MoveStatics extends MiniPhase with SymTransformer { def transformSym(sym: SymDenotation)(using Context): SymDenotation = if (sym.hasAnnotation(defn.ScalaStaticAnnot) && sym.owner.is(Flags.Module) && sym.owner.companionClass.exists && - (sym.is(Flags.Method) || !(sym.is(Flags.Mutable) && sym.owner.companionClass.is(Flags.Trait)))) { + (sym.is(Flags.Method) || !(sym.isMutableVarOrAccessor && sym.owner.companionClass.is(Flags.Trait)))) { sym.owner.asClass.delete(sym.symbol) sym.owner.companionClass.asClass.enter(sym.symbol) sym.copySymDenotation(owner = sym.owner.companionClass) diff --git a/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala b/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala index a9a17f6db464..20b0c8534920 100644 --- a/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala +++ b/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala @@ -8,7 +8,7 @@ import NameKinds.DefaultGetterName import NullOpsDecorator.* import collection.immutable.BitSet import scala.annotation.tailrec -import cc.isCaptureChecking +import cc.{isCaptureChecking, CCState} import scala.compiletime.uninitialized @@ -210,8 +210,7 @@ object OverridingPairs: * @param isSubType A function to be used for checking subtype relationships * between term fields. */ - def isOverridingPair(member: Symbol, memberTp: Type, other: Symbol, otherTp: Type, fallBack: => Boolean = false, - isSubType: (Type, Type) => Context ?=> Boolean = (tp1, tp2) => tp1 frozen_<:< tp2)(using Context): Boolean = + def isOverridingPair(member: Symbol, memberTp: Type, other: Symbol, otherTp: Type, fallBack: => Boolean = false)(using Context): Boolean = if member.isType then // intersection of bounds to refined types must be nonempty memberTp.bounds.hi.hasSameKindAs(otherTp.bounds.hi) && ( @@ -226,6 +225,6 @@ object OverridingPairs: ) else member.name.is(DefaultGetterName) // default getters are not checked for compatibility - || memberTp.overrides(otherTp, member.matchNullaryLoosely || other.matchNullaryLoosely || fallBack, isSubType = isSubType) + || memberTp.overrides(otherTp, member.matchNullaryLoosely || other.matchNullaryLoosely || fallBack) end OverridingPairs diff --git a/compiler/src/dotty/tools/dotc/transform/Pickler.scala b/compiler/src/dotty/tools/dotc/transform/Pickler.scala index fcf1b384fda1..85c06f3e9948 100644 --- a/compiler/src/dotty/tools/dotc/transform/Pickler.scala +++ b/compiler/src/dotty/tools/dotc/transform/Pickler.scala @@ -291,7 +291,7 @@ class Pickler extends Phase { val isOutline = isJavaAttr // TODO: later we may want outline for Scala sources too val attributes = Attributes( sourceFile = sourceRelativePath, - scala2StandardLibrary = ctx.settings.YcompileScala2Library.value, + scala2StandardLibrary = Feature.shouldBehaveAsScala2, explicitNulls = ctx.settings.YexplicitNulls.value, captureChecked = Feature.ccEnabled, withPureFuns = Feature.pureFunsEnabled, diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index df74e102f693..6480ae7fdd05 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -101,7 +101,7 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => private var compilingScala2StdLib = false override def initContext(ctx: FreshContext): Unit = initContextCalled = true - compilingScala2StdLib = ctx.settings.YcompileScala2Library.value(using ctx) + compilingScala2StdLib = Feature.shouldBehaveAsScala2(using ctx) val superAcc: SuperAccessors = new SuperAccessors(thisPhase) val synthMbr: SyntheticMembers = new SyntheticMembers(thisPhase) @@ -132,9 +132,9 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => then false // not an error, but not an expandable unrolled method else if - method.is(Deferred) + method.isLocal + || !method.isEffectivelyFinal || isCtor && method.owner.is(Trait) - || !(isCtor || method.is(Final) || method.owner.is(ModuleClass)) || method.owner.companionClass.is(CaseClass) && (method.name == nme.apply || method.name == nme.fromProduct) || method.owner.is(CaseClass) && method.name == nme.copy @@ -247,8 +247,10 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => if sym.is(Param) then registerIfUnrolledParam(sym) // @unused is getter/setter but we want it on ordinary method params - if !sym.owner.is(Method) || sym.owner.isConstructor then - sym.keepAnnotationsCarrying(thisPhase, Set(defn.ParamMetaAnnot), orNoneOf = defn.NonBeanMetaAnnots) + // @param should be consulted only for fields + val unusing = sym.getAnnotation(defn.UnusedAnnot) + sym.keepAnnotationsCarrying(thisPhase, Set(defn.ParamMetaAnnot), orNoneOf = defn.NonBeanMetaAnnots) + unusing.foreach(sym.addAnnotation) else if sym.is(ParamAccessor) then // @publicInBinary is not a meta-annotation and therefore not kept by `keepAnnotationsCarrying` val publicInBinaryAnnotOpt = sym.getAnnotation(defn.PublicInBinaryAnnot) diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index bd87ebd8abe1..f057959a52b4 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -19,9 +19,7 @@ import typer.ErrorReporting.{Addenda, NothingToAdd} import config.Printers.recheckr import util.Property import StdNames.nme -import reporting.trace import annotation.constructorOnly -import cc.CaptureSet.IdempotentCaptRefMap import annotation.tailrec import dotty.tools.dotc.cc.boxed @@ -167,7 +165,11 @@ abstract class Recheck extends Phase, SymTransformer: * from the current type. */ def setNuType(tpe: Type): Unit = - if nuTypes.lookup(tree) == null && (tpe ne tree.tpe) then nuTypes(tree) = tpe + if nuTypes.lookup(tree) == null then updNuType(tpe) + + /** Set new type of the tree unconditionally. */ + def updNuType(tpe: Type): Unit = + if tpe ne tree.tpe then nuTypes(tree) = tpe /** The new type of the tree, or if none was installed, the original type */ def nuType(using Context): Type = @@ -186,6 +188,9 @@ abstract class Recheck extends Phase, SymTransformer: def keepNuTypes(using Context): Boolean = ctx.settings.Xprint.value.containsPhase(thisPhase) + def resetNuTypes()(using Context): Unit = + nuTypes.clear(resetToInitial = false) + /** A map from NamedTypes to the denotations they had before this phase. * Needed so that we can `reset` them after this phase. */ @@ -219,10 +224,10 @@ abstract class Recheck extends Phase, SymTransformer: sharpen: Denotation => Denotation)(using Context): Type = if name.is(OuterSelectName) then tree.tpe else - //val pre = ta.maybeSkolemizePrefix(qualType, name) + val pre = ta.maybeSkolemizePrefix(qualType, name) val mbr = sharpen( - qualType.findMember(name, qualType, + qualType.findMember(name, pre, excluded = if tree.symbol.is(Private) then EmptyFlags else Private )).suchThat(tree.symbol == _) val newType = tree.tpe match @@ -285,7 +290,7 @@ abstract class Recheck extends Phase, SymTransformer: * The invocation is currently disabled in recheckApply. */ private def mapJavaArgs(formals: List[Type])(using Context): List[Type] = - val tm = new TypeMap with IdempotentCaptRefMap: + val tm = new TypeMap: def apply(t: Type) = t match case t: TypeRef if t.symbol == defn.ObjectClass => defn.FromJavaObjectType @@ -580,7 +585,7 @@ abstract class Recheck extends Phase, SymTransformer: * Otherwise, `tp` itself */ def widenSkolems(tp: Type)(using Context): Type = - object widenSkolems extends TypeMap, IdempotentCaptRefMap: + object widenSkolems extends TypeMap: var didWiden: Boolean = false def apply(t: Type): Type = t match case t: SkolemType if variance >= 0 => @@ -603,6 +608,7 @@ abstract class Recheck extends Phase, SymTransformer: case _ => checkConformsExpr(tpe.widenExpr, pt.widenExpr, tree) def isCompatible(actual: Type, expected: Type)(using Context): Boolean = + try actual <:< expected || expected.isRepeatedParam && isCompatible(actual, @@ -611,6 +617,9 @@ abstract class Recheck extends Phase, SymTransformer: val widened = widenSkolems(expected) (widened ne expected) && isCompatible(actual, widened) } + catch case ex: AssertionError => + println(i"fail while $actual iscompat $expected") + throw ex def checkConformsExpr(actual: Type, expected: Type, tree: Tree, addenda: Addenda = NothingToAdd)(using Context): Type = //println(i"check conforms $actual <:< $expected") diff --git a/compiler/src/dotty/tools/dotc/transform/SpecializeApplyMethods.scala b/compiler/src/dotty/tools/dotc/transform/SpecializeApplyMethods.scala index fd314b94e50c..c85f06e6075f 100644 --- a/compiler/src/dotty/tools/dotc/transform/SpecializeApplyMethods.scala +++ b/compiler/src/dotty/tools/dotc/transform/SpecializeApplyMethods.scala @@ -5,6 +5,7 @@ import ast.Trees.*, ast.tpd, core.* import Contexts.*, Types.*, Decorators.*, Symbols.*, DenotTransformers.* import SymDenotations.*, Scopes.*, StdNames.*, NameOps.*, Names.* import MegaPhase.MiniPhase +import config.Feature import scala.collection.mutable @@ -25,7 +26,7 @@ class SpecializeApplyMethods extends MiniPhase with InfoTransformer { override def description: String = SpecializeApplyMethods.description override def isEnabled(using Context): Boolean = - !ctx.settings.scalajs.value && !ctx.settings.YcompileScala2Library.value + !ctx.settings.scalajs.value && !Feature.shouldBehaveAsScala2 private def specApplySymbol(sym: Symbol, args: List[Type], ret: Type)(using Context): Symbol = { val name = nme.apply.specializedFunction(ret, args) diff --git a/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala b/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala index 926a19224e79..5f1039abec7b 100644 --- a/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala +++ b/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala @@ -10,11 +10,13 @@ import NameOps.* import Annotations.Annotation import typer.ProtoTypes.constrained import ast.untpd +import config.Feature import util.Property import util.Spans.Span import config.Printers.derive import NullOpsDecorator.* +import scala.runtime.Statics object SyntheticMembers { @@ -78,11 +80,11 @@ class SyntheticMembers(thisPhase: DenotTransformer) { private def existingDef(sym: Symbol, clazz: ClassSymbol)(using Context): Symbol = val existing = sym.matchingMember(clazz.thisType) - if ctx.settings.YcompileScala2Library.value && clazz.isValueClass && (sym == defn.Any_equals || sym == defn.Any_hashCode) then + if Feature.shouldBehaveAsScala2 && clazz.isValueClass && (sym == defn.Any_equals || sym == defn.Any_hashCode) then NoSymbol - else if existing != sym && !existing.is(Deferred) then - existing - else + else if existing != sym && !existing.is(Deferred) then + existing + else NoSymbol end existingDef @@ -101,6 +103,7 @@ class SyntheticMembers(thisPhase: DenotTransformer) { val isSimpleEnumValue = isEnumValue && !clazz.owner.isAllOf(EnumCase) val isJavaEnumValue = isEnumValue && clazz.derivesFrom(defn.JavaEnumClass) val isNonJavaEnumValue = isEnumValue && !isJavaEnumValue + val ownName = clazz.name.stripModuleClassSuffix.toString val symbolsToSynthesize: List[Symbol] = if clazz.is(Case) then @@ -124,8 +127,7 @@ class SyntheticMembers(thisPhase: DenotTransformer) { def forwardToRuntime(vrefs: List[Tree]): Tree = ref(defn.runtimeMethodRef("_" + sym.name.toString)).appliedToTermArgs(This(clazz) :: vrefs) - def ownName: Tree = - Literal(Constant(clazz.name.stripModuleClassSuffix.toString)) + def ownNameLit: Tree = Literal(Constant(ownName)) def nameRef: Tree = if isJavaEnumValue then @@ -152,7 +154,7 @@ class SyntheticMembers(thisPhase: DenotTransformer) { Literal(Constant(candidate.get)) def toStringBody(vrefss: List[List[Tree]]): Tree = - if (clazz.is(ModuleClass)) ownName + if (clazz.is(ModuleClass)) ownNameLit else if (isNonJavaEnumValue) identifierRef else forwardToRuntime(vrefss.head) @@ -165,9 +167,9 @@ class SyntheticMembers(thisPhase: DenotTransformer) { case nme.ordinal => ordinalRef case nme.productArity => Literal(Constant(accessors.length)) case nme.productPrefix if isEnumValue => nameRef - case nme.productPrefix => ownName + case nme.productPrefix => ownNameLit case nme.productElement => - if ctx.settings.YcompileScala2Library.value then productElementBodyForScala2Compat(accessors.length, vrefss.head.head) + if Feature.shouldBehaveAsScala2 then productElementBodyForScala2Compat(accessors.length, vrefss.head.head) else productElementBody(accessors.length, vrefss.head.head) case nme.productElementName => productElementNameBody(accessors.length, vrefss.head.head) } @@ -335,39 +337,36 @@ class SyntheticMembers(thisPhase: DenotTransformer) { ref(accessors.head).select(nme.hashCode_).ensureApplied } - /** The class - * - * ``` - * case object C - * ``` - * - * gets the `hashCode` method: - * - * ``` - * def hashCode: Int = "C".hashCode // constant folded - * ``` - * - * The class - * - * ``` - * case class C(x: T, y: U) - * ``` - * - * if none of `T` or `U` are primitive types, gets the `hashCode` method: - * - * ``` - * def hashCode: Int = ScalaRunTime._hashCode(this) - * ``` - * - * else if either `T` or `U` are primitive, gets the `hashCode` method implemented by [[caseHashCodeBody]] + /** + * A `case object C` or a `case class C()` without parameters gets the `hashCode` method + * ``` + * def hashCode: Int = "C".hashCode // constant folded + * ``` + * + * Otherwise, if none of the parameters are primitive types: + * ``` + * def hashCode: Int = MurmurHash3.productHash( + * this, + * Statics.mix(0xcafebabe, "C".hashCode), // constant folded + * ignorePrefix = true) + * ``` + * + * The implementation used to invoke `ScalaRunTime._hashCode`, but that implementation mixes in the result + * of `productPrefix`, which causes scala/bug#13033. By setting `ignorePrefix = true` and mixing in the case + * name into the seed, the bug can be fixed and the generated code works with the unchanged Scala library. + * + * For case classes with primitive paramters, see [[caseHashCodeBody]]. */ def chooseHashcode(using Context) = - if (clazz.is(ModuleClass)) - Literal(Constant(clazz.name.stripModuleClassSuffix.toString.hashCode)) + if (accessors.isEmpty) Literal(Constant(ownName.hashCode)) else if (accessors.exists(_.info.finalResultType.classSymbol.isPrimitiveValueClass)) caseHashCodeBody else - ref(defn.ScalaRuntime__hashCode).appliedTo(This(clazz)) + ref(defn.MurmurHash3Module).select(defn.MurmurHash3_productHash).appliedTo( + This(clazz), + Literal(Constant(Statics.mix(0xcafebabe, ownName.hashCode))), + Literal(Constant(true)) + ) /** The class * @@ -380,7 +379,7 @@ class SyntheticMembers(thisPhase: DenotTransformer) { * ``` * def hashCode: Int = { * var acc: Int = 0xcafebabe - * acc = Statics.mix(acc, this.productPrefix.hashCode()); + * acc = Statics.mix(acc, "C".hashCode); * acc = Statics.mix(acc, x); * acc = Statics.mix(acc, Statics.this.anyHash(y)); * Statics.finalizeHash(acc, 2) @@ -391,7 +390,7 @@ class SyntheticMembers(thisPhase: DenotTransformer) { val acc = newSymbol(ctx.owner, nme.acc, Mutable | Synthetic, defn.IntType, coord = ctx.owner.span) val accDef = ValDef(acc, Literal(Constant(0xcafebabe))) val mixPrefix = Assign(ref(acc), - ref(defn.staticsMethod("mix")).appliedTo(ref(acc), This(clazz).select(defn.Product_productPrefix).select(defn.Any_hashCode).appliedToNone)) + ref(defn.staticsMethod("mix")).appliedTo(ref(acc), Literal(Constant(ownName.hashCode)))) val mixes = for (accessor <- accessors) yield Assign(ref(acc), ref(defn.staticsMethod("mix")).appliedTo(ref(acc), hashImpl(accessor))) val finish = ref(defn.staticsMethod("finalizeHash")).appliedTo(ref(acc), Literal(Constant(accessors.size))) @@ -571,8 +570,9 @@ class SyntheticMembers(thisPhase: DenotTransformer) { newSymbol(ctx.owner, pref.paramName.freshened, Synthetic, pref.underlying.translateFromRepeated(toArray = false), coord = ctx.owner.span.focus) val bindingRefs = bindingSyms.map(TermRef(NoPrefix, _)) - // Fix the infos for dependent parameters - if constrMeth.isParamDependent then + // Fix the infos for dependent parameters. We also need to include false dependencies that would + // be fixed by de-aliasing since we do no such de-aliasing here. See i22944.scala. + if constrMeth.looksParamDependent then bindingSyms.foreach: bindingSym => bindingSym.info = bindingSym.info.substParams(constrMeth, bindingRefs) @@ -721,7 +721,7 @@ class SyntheticMembers(thisPhase: DenotTransformer) { val syntheticMembers = serializableObjectMethod(clazz) ::: serializableEnumValueMethod(clazz) ::: caseAndValueMethods(clazz) checkInlining(syntheticMembers) val impl1 = cpy.Template(impl)(body = syntheticMembers ::: impl.body) - if ctx.settings.YcompileScala2Library.value then impl1 + if Feature.shouldBehaveAsScala2 then impl1 else addMirrorSupport(impl1) } diff --git a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala index 90262bc5da85..2f4ad3a83e4e 100644 --- a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala @@ -244,7 +244,7 @@ object TreeChecker { private val everDefinedSyms = MutableSymbolMap[untpd.Tree]() // don't check value classes after typer, as the constraint about constructors doesn't hold after transform - override def checkDerivedValueClass(clazz: Symbol, stats: List[Tree])(using Context): Unit = () + override def checkDerivedValueClass(cdef: untpd.TypeDef, clazz: Symbol, stats: List[Tree])(using Context): Unit = () def withDefinedSyms[T](trees: List[untpd.Tree])(op: => T)(using Context): T = { var locally = List.empty[Symbol] diff --git a/compiler/src/dotty/tools/dotc/transform/UninitializedDefs.scala b/compiler/src/dotty/tools/dotc/transform/UninitializedDefs.scala index f22fc53e9b6e..7531b6e41c19 100644 --- a/compiler/src/dotty/tools/dotc/transform/UninitializedDefs.scala +++ b/compiler/src/dotty/tools/dotc/transform/UninitializedDefs.scala @@ -33,7 +33,7 @@ class UninitializedDefs extends MiniPhase: def recur(rhs: Tree): Boolean = rhs match case rhs: RefTree => rhs.symbol == defn.Compiletime_uninitialized - && tree.symbol.is(Mutable) && tree.symbol.owner.isClass + && tree.symbol.isMutableVarOrAccessor && tree.symbol.owner.isClass case closureDef(ddef) if defn.isContextFunctionType(tree.tpt.tpe.dealias) => recur(ddef.rhs) case _ => diff --git a/compiler/src/dotty/tools/dotc/transform/UnrollDefinitions.scala b/compiler/src/dotty/tools/dotc/transform/UnrollDefinitions.scala index 44379b88bf16..bf9e20e68930 100644 --- a/compiler/src/dotty/tools/dotc/transform/UnrollDefinitions.scala +++ b/compiler/src/dotty/tools/dotc/transform/UnrollDefinitions.scala @@ -79,7 +79,8 @@ class UnrollDefinitions extends MacroTransform, IdentityDenotTransformer { else Some((paramClauseIndex, annotationIndices)) if indices.nonEmpty then // pre-validation should have occurred in posttyper - assert(annotated.is(Final, butNot = Deferred) || annotated.isConstructor || annotated.owner.is(ModuleClass) || annotated.name.is(DefaultGetterName), + assert(!annotated.isLocal, i"$annotated is local") + assert(annotated.isEffectivelyFinal || annotated.name.is(DefaultGetterName), i"$annotated is not final&concrete, or a constructor") indices }) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 328446a02e23..226e8dbf3fb4 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -1032,7 +1032,7 @@ class Objects(using Context @constructorOnly): UnknownValue else if target.exists then def isNextFieldOfColonColon: Boolean = ref.klass == defn.ConsClass && target.name.toString == "next" - if target.isOneOf(Flags.Mutable) && !isNextFieldOfColonColon then + if target.isMutableVarOrAccessor && !isNextFieldOfColonColon then if ref.hasVar(target) then val addr = ref.varAddr(target) if addr.owner == State.currentObject then diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index adb2370bb1e0..a8a855b6ae5b 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -448,6 +448,18 @@ object Semantic: object TreeCache: class CacheData: private val emptyTrees = mutable.Set[ValOrDefDef]() + private val templatesToSkip = mutable.Set[Template]() + + def checkTemplateBodyValidity(tpl: Template, className: String)(using Context): Unit = + if (templatesToSkip.contains(tpl)) + throw new TastyTreeException(className) + + val errorCount = ctx.reporter.errorCount + tpl.forceFields() + + if (ctx.reporter.errorCount > errorCount) + templatesToSkip.add(tpl) + throw new TastyTreeException(className) extension (tree: ValOrDefDef) def getRhs(using Context): Tree = @@ -465,7 +477,9 @@ object Semantic: if (emptyTrees.contains(tree)) EmptyTree else getTree end TreeCache - + + inline def treeCache(using t: TreeCache.CacheData): TreeCache.CacheData = t + // ----- Operations on domains ----------------------------- extension (a: Value) def join(b: Value): Value = @@ -654,6 +668,8 @@ object Semantic: val methodType = atPhaseBeforeTransforms { meth.info.stripPoly } var allArgsHot = true val allParamTypes = methodType.paramInfoss.flatten.map(_.repeatedToSingle) + if(allParamTypes.size != args.size) + report.warning("[Internal error] Number of parameters do not match number of arguments in " + meth.name) val errors = allParamTypes.zip(args).flatMap { (info, arg) => val tryReporter = Reporter.errorsIn { arg.promote } allArgsHot = allArgsHot && tryReporter.errors.isEmpty @@ -1173,7 +1189,10 @@ object Semantic: given Cache.Data() given TreeCache.CacheData() for classSym <- classes if isConcreteClass(classSym) && !classSym.isStaticObject do - checkClass(classSym) + try + checkClass(classSym) + catch + case TastyTreeException(className) => report.warning("Skipping the analysis of " + classSym.show + " due to an error reading the body of " + className + "'s TASTy.") // ----- Semantic definition -------------------------------- type ArgInfo = TraceValue[Value] @@ -1520,6 +1539,8 @@ object Semantic: * @param klass The class to which the template belongs. */ def init(tpl: Template, thisV: Ref, klass: ClassSymbol): Contextual[Value] = log("init " + klass.show, printer, (_: Value).show) { + treeCache.checkTemplateBodyValidity(tpl, klass.show) + val paramsMap = tpl.constr.termParamss.flatten.map { vdef => vdef.name -> thisV.objekt.field(vdef.symbol) }.toMap diff --git a/compiler/src/dotty/tools/dotc/transform/init/Util.scala b/compiler/src/dotty/tools/dotc/transform/init/Util.scala index e11d0e1e21a5..3280c289f926 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Util.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Util.scala @@ -15,6 +15,9 @@ import config.Printers.init as printer import Trace.* object Util: + /** Exception used for errors encountered when reading TASTy. */ + case class TastyTreeException(msg: String) extends RuntimeException(msg) + /** Utility definition used for better error-reporting of argument errors */ case class TraceValue[T](value: T, trace: Trace) @@ -43,6 +46,8 @@ object Util: case Apply(fn, args) => val argTps = fn.tpe.widen match case mt: MethodType => mt.paramInfos + if (args.size != argTps.size) + report.warning("[Internal error] Number of arguments do not match number of argument types in " + tree.symbol.name) val normArgs: List[Arg] = args.zip(argTps).map { case (arg, _: ExprType) => ByNameArg(arg) case (arg, _) => arg @@ -112,5 +117,5 @@ object Util: /** Whether the class or its super class/trait contains any mutable fields? */ def isMutable(cls: ClassSymbol)(using Context): Boolean = - cls.classInfo.decls.exists(_.is(Flags.Mutable)) || + cls.classInfo.decls.exists(_.isMutableVarOrAccessor) || cls.parentSyms.exists(parentCls => isMutable(parentCls.asClass)) diff --git a/compiler/src/dotty/tools/dotc/transform/localopt/FormatChecker.scala b/compiler/src/dotty/tools/dotc/transform/localopt/FormatChecker.scala index 4922024b6c35..b0ce1d6614fd 100644 --- a/compiler/src/dotty/tools/dotc/transform/localopt/FormatChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/localopt/FormatChecker.scala @@ -5,13 +5,12 @@ import scala.annotation.tailrec import scala.collection.mutable.ListBuffer import scala.util.matching.Regex.Match -import PartialFunction.cond - import dotty.tools.dotc.ast.tpd.{Match => _, *} import dotty.tools.dotc.core.Contexts.* import dotty.tools.dotc.core.Symbols.* import dotty.tools.dotc.core.Types.* import dotty.tools.dotc.core.Phases.typerPhase +import dotty.tools.dotc.reporting.BadFormatInterpolation import dotty.tools.dotc.util.Spans.Span import dotty.tools.dotc.util.chaining.* @@ -29,8 +28,9 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List def argType(argi: Int, types: Type*): Type = require(argi < argc, s"$argi out of range picking from $types") val tpe = argTypes(argi) - types.find(t => argConformsTo(argi, tpe, t)) - .orElse(types.find(t => argConvertsTo(argi, tpe, t))) + types.find(t => t != defn.AnyType && argConformsTo(argi, tpe, t)) + .orElse(types.find(t => t != defn.AnyType && argConvertsTo(argi, tpe, t))) + .orElse(types.find(t => t == defn.AnyType && argConformsTo(argi, tpe, t))) .getOrElse { report.argError(s"Found: ${tpe.show}, Required: ${types.map(_.show).mkString(", ")}", argi) actuals += args(argi) @@ -63,50 +63,57 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List /** For N part strings and N-1 args to interpolate, normalize parts and check arg types. * - * Returns normalized part strings and args, where args correcpond to conversions in tail of parts. + * Returns normalized part strings and args, where args correspond to conversions in tail of parts. */ def checked: (List[String], List[Tree]) = val amended = ListBuffer.empty[String] val convert = ListBuffer.empty[Conversion] + def checkPart(part: String, n: Int): Unit = + val matches = formatPattern.findAllMatchIn(part) + + def insertStringConversion(): Unit = + amended += "%s" + part + val cv = Conversion.stringXn(n) + cv.accepts(argType(n-1, defn.AnyType)) + convert += cv + cv.lintToString(argTypes(n-1)) + + def errorLeading(op: Conversion) = op.errorAt(Spec): + s"conversions must follow a splice; ${Conversion.literalHelp}" + + def accept(op: Conversion): Unit = + if !op.isLeading then errorLeading(op) + op.accepts(argType(n-1, op.acceptableVariants*)) + amended += part + convert += op + op.lintToString(argTypes(n-1)) + + // after the first part, a leading specifier is required for the interpolated arg; %s is supplied if needed + if n == 0 then amended += part + else if !matches.hasNext then insertStringConversion() + else + val cv = Conversion(matches.next(), n) + if cv.isLiteral then insertStringConversion() + else if cv.isIndexed then + if cv.index.getOrElse(-1) == n then accept(cv) else insertStringConversion() + else if !cv.isError then accept(cv) + + // any remaining conversions in this part must be either literals or indexed + while matches.hasNext do + val cv = Conversion(matches.next(), n) + if n == 0 && cv.hasFlag('<') then cv.badFlag('<', "No last arg") + else if !cv.isLiteral && !cv.isIndexed then errorLeading(cv) + end checkPart + @tailrec - def loop(remaining: List[String], n: Int): Unit = - remaining match - case part0 :: more => - def badPart(t: Throwable): String = "".tap(_ => report.partError(t.getMessage.nn, index = n, offset = 0)) - val part = try StringContext.processEscapes(part0) catch badPart - val matches = formatPattern.findAllMatchIn(part) - - def insertStringConversion(): Unit = - amended += "%s" + part - convert += Conversion(formatPattern.findAllMatchIn("%s").next(), n) // improve - argType(n-1, defn.AnyType) - def errorLeading(op: Conversion) = op.errorAt(Spec)(s"conversions must follow a splice; ${Conversion.literalHelp}") - def accept(op: Conversion): Unit = - if !op.isLeading then errorLeading(op) - op.accepts(argType(n-1, op.acceptableVariants*)) - amended += part - convert += op - - // after the first part, a leading specifier is required for the interpolated arg; %s is supplied if needed - if n == 0 then amended += part - else if !matches.hasNext then insertStringConversion() - else - val cv = Conversion(matches.next(), n) - if cv.isLiteral then insertStringConversion() - else if cv.isIndexed then - if cv.index.getOrElse(-1) == n then accept(cv) else insertStringConversion() - else if !cv.isError then accept(cv) - - // any remaining conversions in this part must be either literals or indexed - while matches.hasNext do - val cv = Conversion(matches.next(), n) - if n == 0 && cv.hasFlag('<') then cv.badFlag('<', "No last arg") - else if !cv.isLiteral && !cv.isIndexed then errorLeading(cv) - - loop(more, n + 1) - case Nil => () - end loop + def loop(remaining: List[String], n: Int): Unit = remaining match + case part0 :: remaining => + def badPart(t: Throwable): String = "".tap(_ => report.partError(t.getMessage.nn, index = n, offset = 0)) + val part = try StringContext.processEscapes(part0) catch badPart + checkPart(part, n) + loop(remaining, n + 1) + case Nil => loop(parts, n = 0) if reported then (Nil, Nil) @@ -124,10 +131,8 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List def intOf(g: SpecGroup): Option[Int] = group(g).map(_.toInt) extension (inline value: Boolean) - inline def or(inline body: => Unit): Boolean = value || { body ; false } - inline def orElse(inline body: => Unit): Boolean = value || { body ; true } - inline def and(inline body: => Unit): Boolean = value && { body ; true } - inline def but(inline body: => Unit): Boolean = value && { body ; false } + inline infix def or(inline body: => Unit): Boolean = value || { body; false } + inline infix def and(inline body: => Unit): Boolean = value && { body; true } enum Kind: case StringXn, HashXn, BooleanXn, CharacterXn, IntegralXn, FloatingPointXn, DateTimeXn, LiteralXn, ErrorXn @@ -146,9 +151,10 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List // the conversion char is the head of the op string (but see DateTimeXn) val cc: Char = kind match - case ErrorXn => if op.isEmpty then '?' else op(0) - case DateTimeXn => if op.length > 1 then op(1) else '?' - case _ => op(0) + case ErrorXn => if op.isEmpty then '?' else op(0) + case DateTimeXn => if op.length <= 1 then '?' else op(1) + case StringXn => if op.isEmpty then 's' else op(0) // accommodate the default %s + case _ => op(0) def isIndexed: Boolean = index.nonEmpty || hasFlag('<') def isError: Boolean = kind == ErrorXn @@ -208,18 +214,28 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List // is the specifier OK with the given arg def accepts(arg: Type): Boolean = kind match - case BooleanXn => arg == defn.BooleanType orElse warningAt(CC)("Boolean format is null test for non-Boolean") - case IntegralXn => - arg == BigIntType || !cond(cc) { - case 'o' | 'x' | 'X' if hasAnyFlag("+ (") => "+ (".filter(hasFlag).foreach(bad => badFlag(bad, s"only use '$bad' for BigInt conversions to o, x, X")) ; true - } + case BooleanXn if arg != defn.BooleanType => + warningAt(CC): + """non-Boolean value formats as "true" for non-null references and boxed primitives, otherwise "false"""" + true + case IntegralXn if arg != BigIntType => + cc match + case 'o' | 'x' | 'X' if hasAnyFlag("+ (") => + "+ (".filter(hasFlag).foreach: bad => + badFlag(bad, s"only use '$bad' for BigInt conversions to o, x, X") + false case _ => true + case _ => true + + def lintToString(arg: Type): Unit = + if ctx.settings.Whas.toStringInterpolated && kind == StringXn && !(arg.widen =:= defn.StringType) && !arg.isPrimitiveValueType + then warningAt(CC)("interpolation uses toString") // what arg type if any does the conversion accept def acceptableVariants: List[Type] = kind match case StringXn => if hasFlag('#') then FormattableType :: Nil else defn.AnyType :: Nil - case BooleanXn => defn.BooleanType :: defn.NullType :: Nil + case BooleanXn => defn.BooleanType :: defn.NullType :: defn.AnyType :: Nil // warn if not boolean case HashXn => defn.AnyType :: Nil case CharacterXn => defn.CharType :: defn.ByteType :: defn.ShortType :: defn.IntType :: Nil case IntegralXn => defn.IntType :: defn.LongType :: defn.ByteType :: defn.ShortType :: BigIntType :: Nil @@ -248,25 +264,30 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List object Conversion: def apply(m: Match, i: Int): Conversion = - def kindOf(cc: Char) = cc match - case 's' | 'S' => StringXn - case 'h' | 'H' => HashXn - case 'b' | 'B' => BooleanXn - case 'c' | 'C' => CharacterXn - case 'd' | 'o' | - 'x' | 'X' => IntegralXn - case 'e' | 'E' | - 'f' | - 'g' | 'G' | - 'a' | 'A' => FloatingPointXn - case 't' | 'T' => DateTimeXn - case '%' | 'n' => LiteralXn - case _ => ErrorXn - end kindOf m.group(CC) match - case Some(cc) => new Conversion(m, i, kindOf(cc(0))).tap(_.verify) - case None => new Conversion(m, i, ErrorXn).tap(_.errorAt(Spec)(s"Missing conversion operator in '${m.matched}'; $literalHelp")) + case Some(cc) => + val xn = cc(0) match + case 's' | 'S' => StringXn + case 'h' | 'H' => HashXn + case 'b' | 'B' => BooleanXn + case 'c' | 'C' => CharacterXn + case 'd' | 'o' | + 'x' | 'X' => IntegralXn + case 'e' | 'E' | + 'f' | + 'g' | 'G' | + 'a' | 'A' => FloatingPointXn + case 't' | 'T' => DateTimeXn + case '%' | 'n' => LiteralXn + case _ => ErrorXn + new Conversion(m, i, xn) + .tap(_.verify) + case None => + new Conversion(m, i, ErrorXn) + .tap(_.errorAt(Spec)(s"Missing conversion operator in '${m.matched}'; $literalHelp")) end apply + // construct a default %s conversion + def stringXn(i: Int): Conversion = new Conversion(formatPattern.findAllMatchIn("%").next(), i, StringXn) val literalHelp = "use %% for literal %, %n for newline" end Conversion @@ -276,10 +297,16 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List val pos = partsElems(index).sourcePos val bgn = pos.span.start + offset val fin = if end < 0 then pos.span.end else pos.span.start + end - pos.withSpan(Span(bgn, fin, bgn)) + pos.withSpan(Span(start = bgn, end = fin, point = bgn)) extension (r: report.type) - def argError(message: String, index: Int): Unit = r.error(message, args(index).srcPos).tap(_ => reported = true) - def partError(message: String, index: Int, offset: Int, end: Int = -1): Unit = r.error(message, partPosAt(index, offset, end)).tap(_ => reported = true) - def partWarning(message: String, index: Int, offset: Int, end: Int = -1): Unit = r.warning(message, partPosAt(index, offset, end)).tap(_ => reported = true) + def argError(message: String, index: Int): Unit = + r.error(BadFormatInterpolation(message), args(index).srcPos) + .tap(_ => reported = true) + def partError(message: String, index: Int, offset: Int, end: Int = -1): Unit = + r.error(BadFormatInterpolation(message), partPosAt(index, offset, end)) + .tap(_ => reported = true) + def partWarning(message: String, index: Int, offset: Int, end: Int): Unit = + r.warning(BadFormatInterpolation(message), partPosAt(index, offset, end)) + .tap(_ => reported = true) end TypedFormatChecker diff --git a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala index 7743054f5487..1afcfbac6206 100644 --- a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala +++ b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala @@ -96,16 +96,22 @@ class StringInterpolatorOpt extends MiniPhase: def mkConcat(strs: List[Literal], elems: List[Tree]): Tree = val stri = strs.iterator val elemi = elems.iterator - var result: Tree = stri.next + var result: Tree = stri.next() def concat(tree: Tree): Unit = result = result.select(defn.String_+).appliedTo(tree).withSpan(tree.span) while elemi.hasNext do - concat(elemi.next) - val str = stri.next + val elem = elemi.next() + lintToString(elem) + concat(elem) + val str = stri.next() if !str.const.stringValue.isEmpty then concat(str) result end mkConcat + def lintToString(t: Tree): Unit = + val arg: Type = t.tpe + if ctx.settings.Whas.toStringInterpolated && !(arg.widen =:= defn.StringType) && !arg.isPrimitiveValueType + then report.warning("interpolation uses toString", t.srcPos) val sym = tree.symbol // Test names first to avoid loading scala.StringContext if not used, and common names first val isInterpolatedMethod = diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 058cd2de332c..2505aff0bd33 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -36,6 +36,7 @@ import annotation.threadUnsafe import scala.util.control.NonFatal import dotty.tools.dotc.inlines.Inlines +import scala.annotation.tailrec object Applications { import tpd.* @@ -1526,6 +1527,20 @@ trait Applications extends Compatibility { def trySelectUnapply(qual: untpd.Tree)(fallBack: (Tree, TyperState) => Tree): Tree = { // try first for non-overloaded, then for overloaded occurrences def tryWithName(name: TermName)(fallBack: (Tree, TyperState) => Tree)(using Context): Tree = + /** Returns `true` if there are type parameters after the last explicit + * (non-implicit) term parameters list. + */ + @tailrec + def hasTrailingTypeParams(paramss: List[List[Symbol]], acc: Boolean = false): Boolean = + paramss match + case Nil => acc + case params :: rest => + val newAcc = + params match + case param :: _ if param.isType => true + case param :: _ if param.isTerm && !param.isOneOf(GivenOrImplicit) => false + case _ => acc + hasTrailingTypeParams(paramss.tail, newAcc) def tryWithProto(qual: untpd.Tree, targs: List[Tree], pt: Type)(using Context) = val proto = UnapplyFunProto(pt, this) @@ -1533,7 +1548,13 @@ trait Applications extends Compatibility { val result = if targs.isEmpty then typedExpr(unapp, proto) else typedExpr(unapp, PolyProto(targs, proto)).appliedToTypeTrees(targs) - if !result.symbol.exists + if result.symbol.exists && hasTrailingTypeParams(result.symbol.paramSymss) then + // We don't accept `unapply` or `unapplySeq` methods with type + // parameters after the last explicit term parameter because we + // can't encode them: `UnApply` nodes cannot take type paremeters. + // See #22550 and associated test cases. + notAnExtractor(result) + else if !result.symbol.exists || result.symbol.name == name || ctx.reporter.hasErrors then result diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 2d6817f74ff7..cc43e950ec10 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -37,7 +37,7 @@ import config.Feature, Feature.{sourceVersion, modularity} import config.SourceVersion.* import config.MigrationVersion import printing.Formatting.hlAsKeyword -import cc.{isCaptureChecking, isRetainsLike} +import cc.{isCaptureChecking, isRetainsLike, isUpdateMethod} import collection.mutable import reporting.* @@ -596,7 +596,7 @@ object Checking { if (sym.isConstructor && !sym.isPrimaryConstructor && sym.owner.is(Trait, butNot = JavaDefined)) val addendum = if ctx.settings.Ydebug.value then s" ${sym.owner.flagsString}" else "" fail(em"Traits cannot have secondary constructors$addendum") - checkApplicable(Inline, sym.isTerm && !sym.isOneOf(Mutable | Module)) + checkApplicable(Inline, sym.isTerm && !sym.is(Module) && !sym.isMutableVarOrAccessor) checkApplicable(Lazy, !sym.isOneOf(Method | Mutable)) if (sym.isType && !sym.isOneOf(Deferred | JavaDefined)) for (cls <- sym.allOverriddenSymbols.filter(_.isClass)) { @@ -605,8 +605,12 @@ object Checking { } if sym.isWrappedToplevelDef && !sym.isType && sym.flags.is(Infix, butNot = Extension) then fail(ModifierNotAllowedForDefinition(Flags.Infix, s"A top-level ${sym.showKind} cannot be infix.")) + if sym.isUpdateMethod && !sym.owner.derivesFrom(defn.Caps_Mutable) then + fail(em"Update methods can only be used as members of classes extending the `Mutable` trait") checkApplicable(Erased, - !sym.isOneOf(MutableOrLazy, butNot = Given) && !sym.isType || sym.isClass) + !sym.is(Lazy, butNot = Given) + && !sym.isMutableVarOrAccessor + && (!sym.isType || sym.isClass)) checkCombination(Final, Open) checkCombination(Sealed, Open) checkCombination(Final, Sealed) @@ -739,7 +743,7 @@ object Checking { } /** Verify classes extending AnyVal meet the requirements */ - def checkDerivedValueClass(clazz: Symbol, stats: List[Tree])(using Context): Unit = { + def checkDerivedValueClass(cdef: untpd.TypeDef, clazz: Symbol, stats: List[Tree])(using Context): Unit = { def checkValueClassMember(stat: Tree) = stat match { case _: TypeDef if stat.symbol.isClass => report.error(ValueClassesMayNotDefineInner(clazz, stat.symbol), stat.srcPos) @@ -752,6 +756,14 @@ object Checking { case _ => report.error(ValueClassesMayNotContainInitalization(clazz), stat.srcPos) } + inline def checkParentIsNotAnyValAlias(): Unit = + cdef.rhs match { + case impl: Template => + val parent = impl.parents.head + if parent.symbol.isAliasType && parent.typeOpt.dealias =:= defn.AnyValType then + report.error(ValueClassCannotExtendAliasOfAnyVal(clazz, parent.symbol), cdef.srcPos) + case _ => () + } // We don't check synthesised enum anonymous classes that are generated from // enum extending a value class type (AnyVal or an alias of it) // The error message 'EnumMayNotBeValueClassesID' will take care of generating the error message (See #22236) @@ -766,6 +778,9 @@ object Checking { report.error(ValueClassesMayNotBeAbstract(clazz), clazz.srcPos) if (!clazz.isStatic) report.error(ValueClassesMayNotBeContainted(clazz), clazz.srcPos) + + checkParentIsNotAnyValAlias() + if (isDerivedValueClass(underlyingOfValueClass(clazz.asClass).classSymbol)) report.error(ValueClassesMayNotWrapAnotherValueClass(clazz), clazz.srcPos) else { @@ -774,6 +789,8 @@ object Checking { } clParamAccessors match { case param :: params => + if (defn.isContextFunctionType(param.info)) + report.error("value classes are not allowed for context function types", param.srcPos) if (param.is(Mutable)) report.error(ValueClassParameterMayNotBeAVar(clazz, param), param.srcPos) if (param.info.isInstanceOf[ExprType]) @@ -1301,8 +1318,8 @@ trait Checking { else tpt /** Verify classes extending AnyVal meet the requirements */ - def checkDerivedValueClass(clazz: Symbol, stats: List[Tree])(using Context): Unit = - Checking.checkDerivedValueClass(clazz, stats) + def checkDerivedValueClass(cdef: untpd.TypeDef, clazz: Symbol, stats: List[Tree])(using Context): Unit = + Checking.checkDerivedValueClass(cdef, clazz, stats) /** Check that case classes are not inherited by case classes. */ @@ -1683,7 +1700,7 @@ trait NoChecking extends ReChecking { override def checkNoTargetNameConflict(stats: List[Tree])(using Context): Unit = () override def checkParentCall(call: Tree, caller: ClassSymbol)(using Context): Unit = () override def checkSimpleKinded(tpt: Tree)(using Context): Tree = tpt - override def checkDerivedValueClass(clazz: Symbol, stats: List[Tree])(using Context): Unit = () + override def checkDerivedValueClass(cdef: untpd.TypeDef, clazz: Symbol, stats: List[Tree])(using Context): Unit = () override def checkCaseInheritance(parentSym: Symbol, caseCls: ClassSymbol, pos: SrcPos)(using Context): Unit = () override def checkNoForwardDependencies(vparams: List[ValDef])(using Context): Unit = () override def checkMembersOK(tp: Type, pos: SrcPos)(using Context): Type = tp diff --git a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala index 13e75be75838..58119981dfc4 100644 --- a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala +++ b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala @@ -85,7 +85,7 @@ object ErrorReporting { /** An explanatory note to be added to error messages * when there's a problem with abstract var defs */ def abstractVarMessage(sym: Symbol): String = - if (sym.underlyingSymbol.is(Mutable)) + if sym.underlyingSymbol.isMutableVarOrAccessor then "\n(Note that variables need to be initialized to be defined)" else "" diff --git a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala index 7f040ccd2968..520c8bf62ba4 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala @@ -509,10 +509,15 @@ object Inferencing { } } } - val res = patternBindings.toList.map { (boundSym, _) => + val res = patternBindings.toList.map { (boundSym, origin) => // substitute bounds of pattern bound variables to deal with possible F-bounds for (wildCard, param) <- patternBindings do boundSym.info = boundSym.info.substParam(param, wildCard.typeRef) + + // also substitute in any GADT bounds + // e.g. in i22879, replace the `T` in `X <: Iterable[T]` with the pattern bound `T$1` + ctx.gadtState.replace(origin, boundSym.typeRef) + boundSym } diff --git a/compiler/src/dotty/tools/dotc/typer/Migrations.scala b/compiler/src/dotty/tools/dotc/typer/Migrations.scala index 0e6dc27ecf7f..acf9e7668917 100644 --- a/compiler/src/dotty/tools/dotc/typer/Migrations.scala +++ b/compiler/src/dotty/tools/dotc/typer/Migrations.scala @@ -130,14 +130,24 @@ trait Migrations: def implicitParams(tree: Tree, tp: MethodOrPoly, pt: FunProto)(using Context): Unit = val mversion = mv.ImplicitParamsWithoutUsing if tp.companion == ImplicitMethodType && pt.applyKind != ApplyKind.Using && pt.args.nonEmpty then - val rewriteMsg = Message.rewriteNotice("This code", mversion.patchFrom) + // The application can only be rewritten if it uses parentheses syntax. + // See issue #22927 and related tests. + val hasParentheses = + ctx.source.content + .slice(tree.span.end, pt.args.head.span.start) + .exists(_ == '(') + val rewriteMsg = + if hasParentheses then + Message.rewriteNotice("This code", mversion.patchFrom) + else + "" report.errorOrMigrationWarning( em"""Implicit parameters should be provided with a `using` clause.$rewriteMsg |To disable the warning, please use the following option: | "-Wconf:msg=Implicit parameters should be provided with a `using` clause:s" |""", pt.args.head.srcPos, mversion) - if mversion.needsPatch then + if hasParentheses && mversion.needsPatch then patch(Span(pt.args.head.span.start), "using ") end implicitParams diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 89dc4cf53472..3c9273cdfa2e 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -247,7 +247,7 @@ class Namer { typer: Typer => tree match { case tree: TypeDef if tree.isClassDef => var flags = checkFlags(tree.mods.flags) - if ctx.settings.YcompileScala2Library.value then + if Feature.shouldBehaveAsScala2 then flags |= Scala2x val name = checkNoConflict(tree.name, tree.span).asTypeName val cls = @@ -1223,20 +1223,21 @@ class Namer { typer: Typer => Yes } - def foreachDefaultGetterOf(sym: TermSymbol, op: TermSymbol => Unit): Unit = + def foreachDefaultGetterOf(sym: TermSymbol, alias: TermName)(op: (TermSymbol, TermName) => Unit): Unit = var n = 0 - val methodName = - if sym.name == nme.apply && sym.is(Synthetic) && sym.owner.companionClass.is(Case) then - // The synthesized `apply` methods of case classes use the constructor's default getters - nme.CONSTRUCTOR - else sym.name + // The synthesized `apply` methods of case classes use the constructor's default getters + val useConstructor = sym.name == nme.apply && sym.is(Synthetic) && sym.owner.companionClass.is(Case) + val methodName = if useConstructor then nme.CONSTRUCTOR else sym.name + val aliasedName = if useConstructor then nme.CONSTRUCTOR else alias + val useAliased = !useConstructor && methodName != aliasedName for params <- sym.paramSymss; param <- params do if param.isTerm then if param.is(HasDefault) then val getterName = DefaultGetterName(methodName, n) val getter = pathType.member(getterName).symbol assert(getter.exists, i"$path does not have a default getter named $getterName") - op(getter.asTerm) + val targetName = if useAliased then DefaultGetterName(aliasedName, n) else getterName + op(getter.asTerm, targetName) n += 1 /** Add a forwarder with name `alias` or its type name equivalent to `mbr`, @@ -1358,9 +1359,8 @@ class Namer { typer: Typer => }) buf += ddef.withSpan(span) if hasDefaults then - foreachDefaultGetterOf(sym.asTerm, - getter => addForwarder( - getter.name.asTermName, getter.asSeenFrom(path.tpe), span)) + foreachDefaultGetterOf(sym.asTerm, alias): (getter, getterName) => + addForwarder(getterName, getter.asSeenFrom(path.tpe), span) // adding annotations and flags at the parameter level // TODO: This probably needs to be filtered to avoid adding some annotation @@ -1415,13 +1415,13 @@ class Namer { typer: Typer => addWildcardForwardersNamed(alias, span) def addForwarders(sels: List[untpd.ImportSelector], seen: List[TermName]): Unit = sels match - case sel :: sels1 => + case sel :: sels => if sel.isWildcard then addWildcardForwarders(seen, sel.span) else if !sel.isUnimport then addForwardersNamed(sel.name, sel.rename, sel.span) - addForwarders(sels1, sel.name :: seen) + addForwarders(sels, sel.name :: seen) case _ => /** Avoid a clash of export forwarder `forwarder` with other forwarders in `forwarders`. diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index 310ca999f4c5..86b9a337e69a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -253,7 +253,7 @@ object Nullables: val mutables = infos.foldLeft(Set[TermRef]()): (ms, info) => ms.union( if info.asserted == null then Set.empty - else info.asserted.filter(_.symbol.is(Mutable))) + else info.asserted.filter(_.symbol.isMutableVarOrAccessor)) infos.extendWith(NotNullInfo(Set(), mutables)) end extension @@ -307,7 +307,7 @@ object Nullables: || s.isClass // not in a class || recur(s.owner)) - refSym.is(Mutable) // if it is immutable, we don't need to check the rest conditions + refSym.isMutableVarOrAccessor // if it is immutable, we don't need to check the rest conditions && refOwner.isTerm && recur(ctx.owner) end extension @@ -574,7 +574,7 @@ object Nullables: object dropNotNull extends TreeMap: var dropped: Boolean = false override def transform(t: Tree)(using Context) = t match - case AssertNotNull(t0) if t0.symbol.is(Mutable) => + case AssertNotNull(t0) if t0.symbol.isMutableVarOrAccessor => nullables.println(i"dropping $t") dropped = true transform(t0) diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index 0cc4aaabfc93..379bdbc8d6a0 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -1017,6 +1017,15 @@ object ProtoTypes { paramInfos = tl.paramInfos.mapConserve(wildApprox(_, theMap, seen, internal1).bounds), resType = wildApprox(tl.resType, theMap, seen, internal1) ) + case tp @ AnnotatedType(parent, _) => + // This case avoids approximating types in the annotation tree, which can + // cause the type assigner to fail. + // See #22893 and tests/pos/annot-default-arg-22874.scala. + val parentApprox = wildApprox(parent, theMap, seen, internal) + if tp.isRefining then + WildcardType(TypeBounds.upper(parentApprox)) + else + parentApprox case _ => (if (theMap != null && seen.eq(theMap.seen)) theMap else new WildApproxMap(seen, internal)) .mapOver(tp) diff --git a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala index 59993a69797d..4e7c4336b852 100644 --- a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala +++ b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala @@ -130,7 +130,7 @@ trait QuotesAndSplices { report.error("Open pattern expected an identifier", arg.srcPos) EmptyTree } - for arg <- typedArgs if arg.symbol.is(Mutable) do // TODO support these patterns. Possibly using scala.quoted.util.Var + for arg <- typedArgs if arg.symbol.isMutableVarOrAccessor do // TODO support these patterns. Possibly using scala.quoted.util.Var report.error("References to `var`s cannot be used in higher-order pattern", arg.srcPos) val argTypes = typedArgs.map(_.tpe.widenTermRefExpr) val patType = (tree.typeargs.isEmpty, tree.args.isEmpty) match diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index a015348e90a7..f81c1bf19cb1 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -21,7 +21,7 @@ import config.MigrationVersion import config.Printers.refcheck import reporting.* import Constants.Constant -import cc.stripCapturing +import cc.{stripCapturing, isUpdateMethod, CCState} object RefChecks { import tpd.* @@ -107,7 +107,9 @@ object RefChecks { def checkSelfConforms(other: ClassSymbol) = var otherSelf = other.declaredSelfTypeAsSeenFrom(cls.thisType) if otherSelf.exists then - if !(cinfo.selfType <:< otherSelf) then + if !CCState.withCapAsRoot: // OK? We need this here since self types use `cap` instead of `fresh` + cinfo.selfType <:< otherSelf + then report.error(DoesNotConformToSelfType("illegal inheritance", cinfo.selfType, cls, otherSelf, "parent", other), cls.srcPos) @@ -240,25 +242,24 @@ object RefChecks { && (inLinearizationOrder(sym1, sym2, parent) || parent.is(JavaDefined)) && !sym2.is(AbsOverride) - /** Checks the subtype relationship tp1 <:< tp2. - * It is passed to the `checkOverride` operation in `checkAll`, to be used for - * compatibility checking. - */ - def checkSubType(tp1: Type, tp2: Type)(using Context): Boolean = tp1 frozen_<:< tp2 - /** A hook that allows to omit override checks between `overriding` and `overridden`. * Overridden in capture checking to handle non-capture checked classes leniently. */ def needsCheck(overriding: Symbol, overridden: Symbol)(using Context): Boolean = true - protected def additionalChecks(overriding: Symbol, overridden: Symbol)(using Context): Unit = () + /** Adapt member type and other type so that they can be compared with `frozen_<:<`. + * @return optionally, if adaptation is necessary, the pair of adapted types (memberTp', otherTp') + * Note: we return an Option result to avoid a tuple allocation in the normal case + * where no adaptation is necessary. + */ + def adaptOverridePair(member: Symbol, memberTp: Type, otherTp: Type)(using Context): Option[(Type, Type)] = None - private val subtypeChecker: (Type, Type) => Context ?=> Boolean = this.checkSubType + protected def additionalChecks(overriding: Symbol, overridden: Symbol)(using Context): Unit = () - def checkAll(checkOverride: ((Type, Type) => Context ?=> Boolean, Symbol, Symbol) => Unit) = + def checkAll(checkOverride: (Symbol, Symbol) => Unit) = while hasNext do if needsCheck(overriding, overridden) then - checkOverride(subtypeChecker, overriding, overridden) + checkOverride(overriding, overridden) additionalChecks(overriding, overridden) next() @@ -273,7 +274,7 @@ object RefChecks { if dcl.is(Deferred) then for other <- dcl.allOverriddenSymbols do if !other.is(Deferred) then - checkOverride(subtypeChecker, dcl, other) + checkOverride(dcl, other) end checkAll // Disabled for capture checking since traits can get different parameter refinements @@ -426,19 +427,27 @@ object RefChecks { /* Check that all conditions for overriding `other` by `member` * of class `clazz` are met. */ - def checkOverride(checkSubType: (Type, Type) => Context ?=> Boolean, member: Symbol, other: Symbol): Unit = - def memberTp(self: Type) = + def checkOverride(member: Symbol, other: Symbol): Unit = + def memberType(self: Type) = if (member.isClass) TypeAlias(member.typeRef.etaExpand) else self.memberInfo(member) - def otherTp(self: Type) = - self.memberInfo(other) + def otherType(self: Type) = + self.memberInfo(other) + + var memberTp = memberType(self) + var otherTp = otherType(self) + checker.adaptOverridePair(member, memberTp, otherTp) match + case Some((mtp, otp)) => + memberTp = mtp + otherTp = otp + case None => refcheck.println(i"check override ${infoString(member)} overriding ${infoString(other)}") - def noErrorType = !memberTp(self).isErroneous && !otherTp(self).isErroneous + def noErrorType = !memberTp.isErroneous && !otherTp.isErroneous def overrideErrorMsg(core: Context ?=> String, compareTypes: Boolean = false): Message = - val (mtp, otp) = if compareTypes then (memberTp(self), otherTp(self)) else (NoType, NoType) + val (mtp, otp) = if compareTypes then (memberTp, otherTp) else (NoType, NoType) OverrideError(core, self, member, other, mtp, otp) def compatTypes(memberTp: Type, otherTp: Type): Boolean = @@ -446,8 +455,8 @@ object RefChecks { isOverridingPair(member, memberTp, other, otherTp, fallBack = warnOnMigration( overrideErrorMsg("no longer has compatible type"), - (if (member.owner == clazz) member else clazz).srcPos, version = `3.0`), - isSubType = checkSubType) + (if member.owner == clazz then member else clazz).srcPos, + version = `3.0`)) catch case ex: MissingType => // can happen when called with upwardsSelf as qualifier of memberTp and otherTp, // because in that case we might access types that are not members of the qualifier. @@ -467,7 +476,7 @@ object RefChecks { // with box adaptation, we simply ignore capture annotations here. // This should be safe since the compatibility under box adaptation is already // checked. - memberTp(self).matches(otherTp(self)) + memberTp.matches(otherTp) } def emitOverrideError(fullmsg: Message) = @@ -595,7 +604,7 @@ object RefChecks { overrideError("needs `override` modifier") else if (other.is(AbsOverride) && other.isIncompleteIn(clazz) && !member.is(AbsOverride)) overrideError("needs `abstract override` modifiers") - else if member.is(Override) && other.is(Mutable) then + else if member.is(Override) && other.isMutableVarOrAccessor then overrideError("cannot override a mutable variable") else if (member.isAnyOverride && !(member.owner.thisType.baseClasses exists (_ isSubClass other.owner)) && @@ -616,16 +625,27 @@ object RefChecks { overrideError("is erased, cannot override non-erased member") else if (other.is(Erased) && !member.isOneOf(Erased | Inline)) // (1.9) overrideError("is not erased, cannot override erased member") + else if member.isUpdateMethod && !other.is(Mutable) then + overrideError(i"is an update method, cannot override a read-only method") else if other.is(Inline) && !member.is(Inline) then // (1.10) overrideError("is not inline, cannot implement an inline method") else if (other.isScala2Macro && !member.isScala2Macro) // (1.11) overrideError("cannot be used here - only Scala-2 macros can override Scala-2 macros") - else if !compatTypes(memberTp(self), otherTp(self)) - && !compatTypes(memberTp(upwardsSelf), otherTp(upwardsSelf)) + else if !compatTypes(memberTp, otherTp) && !member.is(Tracked) // Tracked members need to be excluded since they are abstract type members with // singleton types. Concrete overrides usually have a wider type. // TODO: Should we exclude all refinements inherited from parents? + && { + var memberTpUp = memberType(upwardsSelf) + var otherTpUp = otherType(upwardsSelf) + checker.adaptOverridePair(member, memberTpUp, otherTpUp) match + case Some((mtp, otp)) => + memberTpUp = mtp + otherTpUp = otp + case _ => + !compatTypes(memberTpUp, otherTpUp) + } then overrideError("has incompatible type", compareTypes = true) else if (member.targetName != other.targetName) @@ -633,7 +653,7 @@ object RefChecks { overrideError(i"needs to be declared with @targetName(${"\""}${other.targetName}${"\""}) so that external names match") else overrideError("cannot have a @targetName annotation since external names would be different") - else if intoOccurrences(memberTp(self)) != intoOccurrences(otherTp(self)) then + else if intoOccurrences(memberTp) != intoOccurrences(otherTp) then overrideError("has different occurrences of `into` modifiers", compareTypes = true) else if other.is(ParamAccessor) && !isInheritedAccessor(member, other) && !member.is(Tracked) // see remark on tracked members above @@ -775,7 +795,7 @@ object RefChecks { // Give a specific error message for abstract vars based on why it fails: // It could be unimplemented, have only one accessor, or be uninitialized. - if (underlying.is(Mutable)) { + if underlying.isMutableVarOrAccessor then val isMultiple = grouped.getOrElse(underlying.name, Nil).size > 1 // If both getter and setter are missing, squelch the setter error. @@ -784,7 +804,6 @@ object RefChecks { if (member.isSetter) "\n(Note that an abstract var requires a setter in addition to the getter)" else if (member.isGetter && !isMultiple) "\n(Note that an abstract var requires a getter in addition to the setter)" else err.abstractVarMessage(member)) - } else if (underlying.is(Method)) { // If there is a concrete method whose name matches the unimplemented // abstract method, and a cursory examination of the difference reveals @@ -1147,8 +1166,8 @@ object RefChecks { * This check is suppressed if the method is an override. (Because the type of the receiver * may be narrower in the override.) * - * If the extension method is nullary, it is always hidden by a member of the same name. - * (Either the member is nullary, or the reference is taken as the eta-expansion of the member.) + * If the extension method is nilary, it is always hidden by a member of the same name. + * (Either the member is nilary, or the reference is taken as the eta-expansion of the member.) * * This check is in lieu of a more expensive use-site check that an application failed to use an extension. * That check would account for accessibility and opacity. As a limitation, this check considers @@ -1172,25 +1191,29 @@ object RefChecks { extension (tp: Type) def explicit = Applications.stripImplicit(tp.stripPoly, wildcardOnly = true) def hasImplicitParams = tp.stripPoly match { case mt: MethodType => mt.isImplicitMethod case _ => false } + def isNilary = tp.stripPoly match { case mt: MethodType => false case _ => true } val explicitInfo = sym.info.explicit // consider explicit value params - val target0 = explicitInfo.firstParamTypes.head // required for extension method, the putative receiver - val target = target0.dealiasKeepOpaques.typeSymbol.info - val methTp = explicitInfo.resultType // skip leading implicits and the "receiver" parameter - def memberMatchesMethod(member: Denotation) = + def memberHidesMethod(member: Denotation): Boolean = + val methTp = explicitInfo.resultType // skip leading implicits and the "receiver" parameter + if methTp.isNilary then + return true // extension without parens is always hidden by a member of same name val memberIsImplicit = member.info.hasImplicitParams - val paramTps = - if memberIsImplicit then methTp.stripPoly.firstParamTypes - else methTp.explicit.firstParamTypes inline def paramsCorrespond = + val paramTps = + if memberIsImplicit then methTp.stripPoly.firstParamTypes + else methTp.explicit.firstParamTypes val memberParamTps = member.info.stripPoly.firstParamTypes memberParamTps.corresponds(paramTps): (m, x) => m.typeSymbol.denot.isOpaqueAlias == x.typeSymbol.denot.isOpaqueAlias && (x frozen_<:< m) - paramTps.isEmpty || memberIsImplicit && !methTp.hasImplicitParams || paramsCorrespond - def hidden = - target.nonPrivateMember(sym.name) - .filterWithPredicate: member => - member.symbol.isPublic && memberMatchesMethod(member) - .exists + memberIsImplicit && !methTp.hasImplicitParams || paramsCorrespond + def targetOfHiddenExtension: Symbol = + val target = + val target0 = explicitInfo.firstParamTypes.head // required for extension method, the putative receiver + target0.dealiasKeepOpaques.typeSymbol.info + val member = target.nonPrivateMember(sym.name) + .filterWithPredicate: member => + member.symbol.isPublic && memberHidesMethod(member) + if member.exists then target.typeSymbol else NoSymbol if sym.is(HasDefaultParams) then val getterDenot = val receiverName = explicitInfo.firstParamNames.head @@ -1199,8 +1222,10 @@ object RefChecks { sym.owner.info.member(getterName) if getterDenot.exists then report.warning(ExtensionHasDefault(sym), getterDenot.symbol.srcPos) - if !sym.nextOverriddenSymbol.exists && hidden - then report.warning(ExtensionNullifiedByMember(sym, target.typeSymbol), sym.srcPos) + if !sym.nextOverriddenSymbol.exists then + val target = targetOfHiddenExtension + if target.exists then + report.warning(ExtensionNullifiedByMember(sym, target), sym.srcPos) end checkExtensionMethods /** Verify that references in the user-defined `@implicitNotFound` message are valid. diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index be3186720fa1..21a2acc69ac1 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1393,7 +1393,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer cpy.Assign(tree)(lhsCore, typed(tree.rhs, lhs1.tpe.widen)).withType(defn.UnitType) def canAssign(sym: Symbol) = - sym.is(Mutable, butNot = Accessor) || + sym.isMutableVar || ctx.owner.isPrimaryConstructor && !sym.is(Method) && sym.maybeOwner == ctx.owner.owner || // allow assignments from the primary constructor to class fields ctx.owner.name.is(TraitSetterName) || ctx.owner.isStaticConstructor @@ -2805,8 +2805,10 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer else assert(ctx.reporter.errorsReported) tree.withType(defn.AnyType) + val savedGadt = nestedCtx.gadt val trees1 = tree.trees.mapconserve(typed(_, pt)(using nestedCtx)) .mapconserve(ensureValueTypeOrWildcard) + nestedCtx.gadtState.restore(savedGadt) // Disable GADT reasoning for pattern alternatives assignType(cpy.Alternative(tree)(trees1), trees1) } @@ -3204,7 +3206,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer checkEnumParent(cls, firstParent) - if defn.ScalaValueClasses()(cls) && ctx.settings.YcompileScala2Library.value then + if defn.ScalaValueClasses()(cls) && Feature.shouldBehaveAsScala2 then constr1.symbol.resetFlag(Private) val self1 = typed(self)(using ctx.outer).asInstanceOf[ValDef] // outer context where class members are not visible @@ -3241,7 +3243,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer checkNonCyclicInherited(cls.thisType, cls.info.parents, cls.info.decls, cdef.srcPos) // check value class constraints - checkDerivedValueClass(cls, body1) + checkDerivedValueClass(cdef, cls, body1) val effectiveOwner = cls.owner.skipWeakOwner if cls.is(ModuleClass) diff --git a/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala b/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala index 3699ca80d011..0c2929283ee3 100644 --- a/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala +++ b/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala @@ -157,7 +157,7 @@ class VarianceChecker(using Context) { def isLocal = base.isAllOf(PrivateLocal) || base.is(Private) && !base.hasAnnotation(defn.AssignedNonLocallyAnnot) - if base.is(Mutable, butNot = Method) && !isLocal then + if base.isMutableVar && !isLocal then base.removeAnnotation(defn.AssignedNonLocallyAnnot) variance = 0 try checkInfo(base.info) diff --git a/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala b/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala index b243145c9e5f..714e3a5fc0d6 100644 --- a/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala +++ b/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala @@ -18,12 +18,18 @@ abstract class SimpleIdentitySet[+Elem <: AnyRef] { var acc: SimpleIdentitySet[B] = SimpleIdentitySet.empty foreach(x => acc += f(x)) acc + def flatMap[B <: AnyRef](f: Elem => SimpleIdentitySet[B]): SimpleIdentitySet[B] = + var acc: SimpleIdentitySet[B] = SimpleIdentitySet.empty + foreach(x => acc ++= f(x)) + acc def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A def toList: List[Elem] - def iterator: Iterator[Elem] + def nth(n: Int): Elem final def isEmpty: Boolean = size == 0 + final def iterator: Iterator[Elem] = Iterator.tabulate(size)(nth) + def forall[E >: Elem <: AnyRef](p: E => Boolean): Boolean = !exists(!p(_)) def filter(p: Elem => Boolean): SimpleIdentitySet[Elem] = @@ -42,6 +48,11 @@ abstract class SimpleIdentitySet[+Elem <: AnyRef] { if (that.contains(x)) s else s + x } + def ** [E >: Elem <: AnyRef](that: SimpleIdentitySet[E]): SimpleIdentitySet[E] = + if this.size == 0 then this + else if that.size == 0 then that + else this.filter(that.contains) + def == [E >: Elem <: AnyRef](that: SimpleIdentitySet[E]): Boolean = this.size == that.size && forall(that.contains) @@ -69,7 +80,7 @@ object SimpleIdentitySet { override def map[B <: AnyRef](f: Nothing => B): SimpleIdentitySet[B] = empty def /: [A, E <: AnyRef](z: A)(f: (A, E) => A): A = z def toList = Nil - def iterator = Iterator.empty + def nth(n: Int): Nothing = throw new IndexOutOfBoundsException(n.toString) } private class Set1[+Elem <: AnyRef](x0: AnyRef) extends SimpleIdentitySet[Elem] { @@ -87,7 +98,9 @@ object SimpleIdentitySet { def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A = f(z, x0.asInstanceOf[E]) def toList = x0.asInstanceOf[Elem] :: Nil - def iterator = Iterator.single(x0.asInstanceOf[Elem]) + def nth(n: Int) = + if n == 0 then x0.asInstanceOf[Elem] + else throw new IndexOutOfBoundsException(n.toString) } private class Set2[+Elem <: AnyRef](x0: AnyRef, x1: AnyRef) extends SimpleIdentitySet[Elem] { @@ -109,10 +122,10 @@ object SimpleIdentitySet { def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A = f(f(z, x0.asInstanceOf[E]), x1.asInstanceOf[E]) def toList = x0.asInstanceOf[Elem] :: x1.asInstanceOf[Elem] :: Nil - def iterator = Iterator.tabulate(2) { + def nth(n: Int) = n match case 0 => x0.asInstanceOf[Elem] case 1 => x1.asInstanceOf[Elem] - } + case _ => throw new IndexOutOfBoundsException(n.toString) } private class Set3[+Elem <: AnyRef](x0: AnyRef, x1: AnyRef, x2: AnyRef) extends SimpleIdentitySet[Elem] { @@ -149,11 +162,11 @@ object SimpleIdentitySet { def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A = f(f(f(z, x0.asInstanceOf[E]), x1.asInstanceOf[E]), x2.asInstanceOf[E]) def toList = x0.asInstanceOf[Elem] :: x1.asInstanceOf[Elem] :: x2.asInstanceOf[Elem] :: Nil - def iterator = Iterator.tabulate(3) { + def nth(n: Int) = n match case 0 => x0.asInstanceOf[Elem] case 1 => x1.asInstanceOf[Elem] case 2 => x2.asInstanceOf[Elem] - } + case _ => throw new IndexOutOfBoundsException(n.toString) } private class SetN[+Elem <: AnyRef](val xs: Array[AnyRef]) extends SimpleIdentitySet[Elem] { @@ -200,7 +213,9 @@ object SimpleIdentitySet { foreach(buf += _) buf.toList } - def iterator = xs.iterator.asInstanceOf[Iterator[Elem]] + def nth(n: Int) = + if 0 <= n && n < size then xs(n).asInstanceOf[Elem] + else throw new IndexOutOfBoundsException(n.toString) override def ++ [E >: Elem <: AnyRef](that: SimpleIdentitySet[E]): SimpleIdentitySet[E] = that match { case that: SetN[?] => diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index 6f682ac96a1a..0b67c36492ae 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -188,7 +188,7 @@ class ReplDriver(settings: Array[String], /* complete = */ false // if true adds space when completing ) } - val comps = completionsWithSignatures(line.cursor, line.line, state) + val comps = completions(line.cursor, line.line, state) candidates.addAll(comps.map(_.label).distinct.map(makeCandidate).asJava) val lineWord = line.word() comps.filter(c => c.label == lineWord && c.symbols.nonEmpty) match @@ -275,22 +275,8 @@ class ReplDriver(settings: Array[String], else label - @deprecated("Use completionsWithSignatures instead", "3.4.2") - protected final def completions(cursor: Int, expr: String, state0: State): List[Candidate] = - completionsWithSignatures(cursor, expr, state0).map: c => - new Candidate( - /* value = */ c.label, - /* displ = */ stripBackTicks(c.label), // displayed value - /* group = */ null, // can be used to group completions together - /* descr = */ null, // TODO use for documentation? - /* suffix = */ null, - /* key = */ null, - /* complete = */ false // if true adds space when completing - ) - end completions - /** Extract possible completions at the index of `cursor` in `expr` */ - protected final def completionsWithSignatures(cursor: Int, expr: String, state0: State): List[Completion] = + protected final def completions(cursor: Int, expr: String, state0: State): List[Completion] = if expr.startsWith(":") then ParseResult.commands.collect { case command if command._1.startsWith(expr) => Completion(command._1, "", List()) @@ -309,7 +295,7 @@ class ReplDriver(settings: Array[String], try Completion.completions(srcPos)._2 catch case NonFatal(_) => Nil } .getOrElse(Nil) - end completionsWithSignatures + end completions protected def interpret(res: ParseResult)(using state: State): State = { res match { diff --git a/compiler/test-resources/repl/i13181 b/compiler/test-resources/repl/i13181 index 32f6c6e40c1e..c2c48f9d4214 100644 --- a/compiler/test-resources/repl/i13181 +++ b/compiler/test-resources/repl/i13181 @@ -1,2 +1,2 @@ scala> scala.compiletime.codeOf(1+2) -val res0: String = 1.+(2) +val res0: String = 1 + 2 diff --git a/compiler/test-resources/scripting/envtestNu.sc b/compiler/test-resources/scripting/envtestNu.sc deleted file mode 100755 index fe4cd7851b0a..000000000000 --- a/compiler/test-resources/scripting/envtestNu.sc +++ /dev/null @@ -1,2 +0,0 @@ -// MIGRATION: Scala CLI expects `*.sc` files to be straight-line code - println("Hello " + util.Properties.propOrNull("key")) diff --git a/compiler/test-resources/scripting/scriptPathNu.sc b/compiler/test-resources/scripting/scriptPathNu.sc deleted file mode 100755 index bb3e459654b9..000000000000 --- a/compiler/test-resources/scripting/scriptPathNu.sc +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bin/scala - -// THIS FILE IS RAN WITH SCALA CLI, which wraps scripts exposing scriptPath and args variables - -args.zipWithIndex.foreach { case (arg,i) => printf("arg %d: [%s]\n",i,arg) } - -if !scriptPath.endsWith("scriptPathNu.sc") then - printf( s"incorrect script.path defined as [$scriptPath]") -else - printf("scriptPath: %s\n", scriptPath) // report the value - -extension(s: String) - def norm: String = s.replace('\\', '/') diff --git a/compiler/test-resources/scripting/showArgsNu.sc b/compiler/test-resources/scripting/showArgsNu.sc deleted file mode 100755 index f4c1aa6af257..000000000000 --- a/compiler/test-resources/scripting/showArgsNu.sc +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bin/scala - -// precise output format expected by BashScriptsTests.scala -// MIGRATION: Scala CLI expects `*.sc` files to be straight-line code -for (a,i) <- args.zipWithIndex do - printf(s"arg %2d:[%s]\n",i,a) diff --git a/compiler/test-resources/scripting/sqlDateErrorNu.sc b/compiler/test-resources/scripting/sqlDateErrorNu.sc deleted file mode 100755 index a6f1bd50297d..000000000000 --- a/compiler/test-resources/scripting/sqlDateErrorNu.sc +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bin/scala - -// def main(args: Array[String]): Unit = { MIGRATION: Scala CLI expects `*.sc` files to be straight-line code - println(new java.sql.Date(100L)) - System.err.println("SCALA_OPTS="+Option(System.getenv("SCALA_OPTS")).getOrElse("")) -// } diff --git a/compiler/test/dotc/pos-test-pickling.excludelist b/compiler/test/dotc/pos-test-pickling.excludelist index 07c157793f5d..28bce963bfd1 100644 --- a/compiler/test/dotc/pos-test-pickling.excludelist +++ b/compiler/test/dotc/pos-test-pickling.excludelist @@ -24,6 +24,7 @@ t5031_2.scala i16997.scala i7414.scala i17588.scala +i8300.scala i9804.scala i13433.scala i16649-irrefutable.scala @@ -70,6 +71,7 @@ i18211.scala named-tuples1.scala i20897.scala i20512.scala +i22645b.scala # Opaque type i5720.scala diff --git a/compiler/test/dotc/run-test-pickling.excludelist b/compiler/test/dotc/run-test-pickling.excludelist index c880a4b78f23..1597487da668 100644 --- a/compiler/test/dotc/run-test-pickling.excludelist +++ b/compiler/test/dotc/run-test-pickling.excludelist @@ -49,4 +49,4 @@ named-tuples-strawman-2.scala # typecheckErrors method unpickling typeCheckErrors.scala i18150.scala - +i22968.scala diff --git a/compiler/test/dotty/Properties.scala b/compiler/test/dotty/Properties.scala index 86e0788a3b8f..d937fff6242d 100644 --- a/compiler/test/dotty/Properties.scala +++ b/compiler/test/dotty/Properties.scala @@ -19,7 +19,6 @@ object Properties { /** Are we running on the CI? */ val isRunByCI: Boolean = sys.env.isDefinedAt("DOTTY_CI_RUN") - || sys.env.isDefinedAt("DRONE") // TODO remove this when we drop Drone val testCache: Path = sys.env.get("DOTTY_TEST_CACHE").map(Paths.get(_)).getOrElse { diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index e62c80d7bff7..93238647d1a3 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -83,7 +83,9 @@ class CompilationTests { compileFile("tests/rewrites/ambiguous-named-tuple-assignment.scala", defaultOptions.and("-rewrite", "-source:3.6-migration")), compileFile("tests/rewrites/i21382.scala", defaultOptions.and("-indent", "-rewrite")), compileFile("tests/rewrites/unused.scala", defaultOptions.and("-rewrite", "-Wunused:all")), - compileFile("tests/rewrites/i22440.scala", defaultOptions.and("-rewrite")) + compileFile("tests/rewrites/i22440.scala", defaultOptions.and("-rewrite")), + compileFile("tests/rewrites/i22731.scala", defaultOptions.and("-rewrite", "-source:3.7-migration")), + compileFile("tests/rewrites/i22731b.scala", defaultOptions.and("-rewrite", "-source:3.7-migration")), ).checkRewrites() } @@ -273,20 +275,45 @@ class CompilationTests { * compatible, but (b) and (c) are not. If (b) and (c) are compiled together, there should be * an error when reading the files' TASTy trees. */ locally { - val tastyErrorGroup = TestGroup("checkInit/tasty-error") + val tastyErrorGroup = TestGroup("checkInit/tasty-error/val-or-defdef") val tastyErrorOptions = options.without("-Xfatal-warnings") - val a0Dir = defaultOutputDir + tastyErrorGroup + "/A/v0/A" - val a1Dir = defaultOutputDir + tastyErrorGroup + "/A/v1/A" - val b1Dir = defaultOutputDir + tastyErrorGroup + "/B/v1/B" + val classA0 = defaultOutputDir + tastyErrorGroup + "/A/v0/A" + val classA1 = defaultOutputDir + tastyErrorGroup + "/A/v1/A" + val classB1 = defaultOutputDir + tastyErrorGroup + "/B/v1/B" val tests = List( - compileFile("tests/init/tasty-error/v1/A.scala", tastyErrorOptions)(tastyErrorGroup), - compileFile("tests/init/tasty-error/v1/B.scala", tastyErrorOptions.withClasspath(a1Dir))(tastyErrorGroup), - compileFile("tests/init/tasty-error/v0/A.scala", tastyErrorOptions)(tastyErrorGroup), + compileFile("tests/init/tasty-error/val-or-defdef/v1/A.scala", tastyErrorOptions)(tastyErrorGroup), + compileFile("tests/init/tasty-error/val-or-defdef/v1/B.scala", tastyErrorOptions.withClasspath(classA1))(tastyErrorGroup), + compileFile("tests/init/tasty-error/val-or-defdef/v0/A.scala", tastyErrorOptions)(tastyErrorGroup), ).map(_.keepOutput.checkCompile()) - compileFile("tests/init/tasty-error/Main.scala", tastyErrorOptions.withClasspath(a0Dir).withClasspath(b1Dir))(tastyErrorGroup).checkExpectedErrors() + compileFile("tests/init/tasty-error/val-or-defdef/Main.scala", tastyErrorOptions.withClasspath(classA0).withClasspath(classB1))(tastyErrorGroup).checkExpectedErrors() + + tests.foreach(_.delete()) + } + + /* This tests for errors in the program's TASTy trees. + * The test consists of five files: Main, C, v1/A, v1/B, and v0/A. The files v1/A, v1/B, and v0/A all depend on C. v1/A and v1/B are + * compatible, but v1/B and v0/A are not. If v1/B and v0/A are compiled together, there should be + * an error when reading the files' TASTy trees. This fact is demonstrated by the compilation of Main. */ + locally { + val tastyErrorGroup = TestGroup("checkInit/tasty-error/typedef") + val tastyErrorOptions = options.without("-Xfatal-warnings").without("-Ycheck:all") + + val classC = defaultOutputDir + tastyErrorGroup + "/C/typedef/C" + val classA0 = defaultOutputDir + tastyErrorGroup + "/A/v0/A" + val classA1 = defaultOutputDir + tastyErrorGroup + "/A/v1/A" + val classB1 = defaultOutputDir + tastyErrorGroup + "/B/v1/B" + + val tests = List( + compileFile("tests/init/tasty-error/typedef/C.scala", tastyErrorOptions)(tastyErrorGroup), + compileFile("tests/init/tasty-error/typedef/v1/A.scala", tastyErrorOptions.withClasspath(classC))(tastyErrorGroup), + compileFile("tests/init/tasty-error/typedef/v1/B.scala", tastyErrorOptions.withClasspath(classC).withClasspath(classA1))(tastyErrorGroup), + compileFile("tests/init/tasty-error/typedef/v0/A.scala", tastyErrorOptions.withClasspath(classC))(tastyErrorGroup), + ).map(_.keepOutput.checkCompile()) + + compileFile("tests/init/tasty-error/typedef/Main.scala", tastyErrorOptions.withClasspath(classC).withClasspath(classA0).withClasspath(classB1))(tastyErrorGroup).checkExpectedErrors() tests.foreach(_.delete()) } diff --git a/compiler/test/dotty/tools/dotc/config/ScalaSettingsTests.scala b/compiler/test/dotty/tools/dotc/config/ScalaSettingsTests.scala index 07834684d33b..c74be4901137 100644 --- a/compiler/test/dotty/tools/dotc/config/ScalaSettingsTests.scala +++ b/compiler/test/dotty/tools/dotc/config/ScalaSettingsTests.scala @@ -299,4 +299,11 @@ class ScalaSettingsTests: ) assertEquals(result, Right(reporting.Action.Error)) + @Test def `illegal source versions are not accepted when parsing the settings`: Unit = + for source <- SourceVersion.illegalInSettings do + val settings = ScalaSettings + val result = settings.processArguments(List("-source", source.toString()), true) + assertEquals(0, result.warnings.length) + assertEquals(1, result.errors.length) + end ScalaSettingsTests diff --git a/compiler/test/dotty/tools/dotc/core/NameOpsTest.scala b/compiler/test/dotty/tools/dotc/core/NameOpsTest.scala new file mode 100644 index 000000000000..947ca482fd6f --- /dev/null +++ b/compiler/test/dotty/tools/dotc/core/NameOpsTest.scala @@ -0,0 +1,15 @@ +package dotty.tools.dotc.core + +import dotty.tools.dotc.core.NameOps.isOperatorName +import dotty.tools.dotc.core.Names.{termName, SimpleName} + +import org.junit.Test + +class NameOpsTest: + @Test def isOperatorNamePos: Unit = + for name <- List("+", "::", "frozen_=:=", "$_+", "a2_+", "a_b_+") do + assert(isOperatorName(termName(name))) + + @Test def isOperatorNameNeg: Unit = + for name <- List("foo", "*_*", "", "$reserved", "a*", "2*") do + assert(!isOperatorName(termName(name))) diff --git a/compiler/test/dotty/tools/repl/ReplTest.scala b/compiler/test/dotty/tools/repl/ReplTest.scala index 9741a8dee450..a0975d603ae1 100644 --- a/compiler/test/dotty/tools/repl/ReplTest.scala +++ b/compiler/test/dotty/tools/repl/ReplTest.scala @@ -42,7 +42,7 @@ extends ReplDriver(options, new PrintStream(out, true, StandardCharsets.UTF_8.na /** Returns the `(, )`*/ def tabComplete(src: String)(implicit state: State): List[String] = - completionsWithSignatures(src.length, src, state).map(_.label).sorted.distinct + completions(src.length, src, state).map(_.label).sorted.distinct extension [A](state: State) infix def andThen(op: State ?=> A): A = op(using state) diff --git a/compiler/test/dotty/tools/scripting/ScriptingTests.scala b/compiler/test/dotty/tools/scripting/ScriptingTests.scala index 4dc193f0efe4..8d07cb137917 100644 --- a/compiler/test/dotty/tools/scripting/ScriptingTests.scala +++ b/compiler/test/dotty/tools/scripting/ScriptingTests.scala @@ -51,10 +51,7 @@ class ScriptingTests: */ @Test def scriptingMainTests = assumeFalse("Scripts do not yet support Scala 2 library TASTy", Properties.usingScalaLibraryTasty) - for - (scriptFile, scriptArgs) <- scalaFilesWithArgs(".sc") - if !scriptFile.getName().endsWith("Nu.sc") - do + for (scriptFile, scriptArgs) <- scalaFilesWithArgs(".sc") do showScriptUnderTest(scriptFile) val unexpectedJar = script2jar(scriptFile) unexpectedJar.delete @@ -73,10 +70,7 @@ class ScriptingTests: */ @Test def scriptingJarTest = assumeFalse("Scripts do not yet support Scala 2 library TASTy", Properties.usingScalaLibraryTasty) - for - (scriptFile, scriptArgs) <- scalaFilesWithArgs(".sc") - if !scriptFile.getName().endsWith("Nu.sc") - do + for (scriptFile, scriptArgs) <- scalaFilesWithArgs(".sc") do showScriptUnderTest(scriptFile) val expectedJar = script2jar(scriptFile) expectedJar.delete diff --git a/compiler/test/dotty/tools/utils.scala b/compiler/test/dotty/tools/utils.scala index c33310acf06e..03e25630c421 100644 --- a/compiler/test/dotty/tools/utils.scala +++ b/compiler/test/dotty/tools/utils.scala @@ -124,6 +124,7 @@ private val toolArg = raw"(?://|/\*| \*) ?(?i:(${ToolName.values.mkString("|")}) private val directiveOptionsArg = raw"//> using options (.*)".r.unanchored private val directiveJavacOptions = raw"//> using javacOpt (.*)".r.unanchored private val directiveTargetOptions = raw"//> using target.platform (jvm|scala-js)".r.unanchored +private val directiveUnsupported = raw"//> using (scala) (.*)".r.unanchored private val directiveUnknown = raw"//> using (.*)".r.unanchored // Inspect the lines for compiler options of the form @@ -141,6 +142,7 @@ def toolArgsParse(lines: List[String], filename: Option[String]): List[(String,S case directiveOptionsArg(args) => List(("scalac", args)) case directiveJavacOptions(args) => List(("javac", args)) case directiveTargetOptions(platform) => List(("target", platform)) + case directiveUnsupported(name, args) => Nil case directiveUnknown(rest) => sys.error(s"Unknown directive: `//> using ${CommandLineParser.tokenize(rest).headOption.getOrElse("''")}`${filename.fold("")(f => s" in file $f")}") case _ => Nil } diff --git a/compiler/test/dotty/tools/vulpix/FileFilter.scala b/compiler/test/dotty/tools/vulpix/FileFilter.scala index 7bb098e1903e..b59b4d4f209d 100644 --- a/compiler/test/dotty/tools/vulpix/FileFilter.scala +++ b/compiler/test/dotty/tools/vulpix/FileFilter.scala @@ -23,8 +23,4 @@ object FileFilter { object NoFilter extends FileFilter { def accept(file: String) = true } - - object ExcludeDotFiles extends FileFilter { - def accept(file: String) = !file.startsWith(".") - } } diff --git a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala index 7656acb38f70..12a53a19931d 100644 --- a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala +++ b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala @@ -1423,7 +1423,7 @@ trait ParallelTesting extends RunnerOrchestration { self => private def compilationTargets(sourceDir: JFile, fileFilter: FileFilter = FileFilter.NoFilter): (List[JFile], List[JFile]) = sourceDir.listFiles.foldLeft((List.empty[JFile], List.empty[JFile])) { case ((dirs, files), f) => if (!fileFilter.accept(f.getName)) (dirs, files) - else if (f.isDirectory && FileFilter.ExcludeDotFiles.accept(f.getName)) (f :: dirs, files) + else if (f.isDirectory) (f :: dirs, files) else if (isSourceFile(f)) (dirs, f :: files) else (dirs, files) } diff --git a/compiler/test/dotty/tools/vulpix/VulpixUnitTests.scala b/compiler/test/dotty/tools/vulpix/VulpixUnitTests.scala index baf61c845d96..ab8a611caa33 100644 --- a/compiler/test/dotty/tools/vulpix/VulpixUnitTests.scala +++ b/compiler/test/dotty/tools/vulpix/VulpixUnitTests.scala @@ -105,7 +105,7 @@ object VulpixUnitTests extends ParallelTesting { def maxDuration = 3.seconds def numberOfSlaves = 5 def safeMode = sys.env.get("SAFEMODE").isDefined - def isInteractive = !sys.env.contains("DRONE") + def isInteractive = !sys.env.contains("DOTTY_CI_RUN") def testFilter = Nil def updateCheckFiles: Boolean = false def failedTests = None diff --git a/dist/bin-native-overrides/cli-common-platform b/dist/bin-native-overrides/cli-common-platform deleted file mode 100644 index 1a11c770f91a..000000000000 --- a/dist/bin-native-overrides/cli-common-platform +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -if [[ ${cygwin-} || ${mingw-} || ${msys-} ]]; then - SCALA_CLI_VERSION="" - # iterate through lines in VERSION_SRC - while IFS= read -r line; do - # if line starts with "version:=" then extract the version - if [[ "$line" == cli_version:=* ]]; then - SCALA_CLI_VERSION="${line#cli_version:=}" - break - fi - done < "$PROG_HOME/EXTRA_PROPERTIES" - SCALA_CLI_CMD_BASH=("\"$PROG_HOME/bin/scala-cli\"" "--cli-version \"$SCALA_CLI_VERSION\"") -else - SCALA_CLI_CMD_BASH=("\"$PROG_HOME/bin/scala-cli\"") -fi diff --git a/dist/bin-native-overrides/cli-common-platform.bat b/dist/bin-native-overrides/cli-common-platform.bat deleted file mode 100644 index d1c4f1c4716b..000000000000 --- a/dist/bin-native-overrides/cli-common-platform.bat +++ /dev/null @@ -1,22 +0,0 @@ -@echo off - -setlocal enabledelayedexpansion - -set "_SCALA_CLI_VERSION=" -@rem read for cli_version:=_SCALA_CLI_VERSION in EXTRA_PROPERTIES file -FOR /F "usebackq delims=" %%G IN ("%_PROG_HOME%\EXTRA_PROPERTIES") DO ( - SET "line=%%G" - IF "!line:~0,13!"=="cli_version:=" ( - SET "_SCALA_CLI_VERSION=!line:~13!" - GOTO :foundCliVersion - ) -) - -@REM we didn't find it, so we should fail -echo "ERROR: cli_version not found in EXTRA_PROPERTIES file" -exit /b 1 - -:foundCliVersion -endlocal & set "SCALA_CLI_VERSION=%_SCALA_CLI_VERSION%" - -set SCALA_CLI_CMD_WIN="%_PROG_HOME%\bin\scala-cli.exe" "--cli-version" "%SCALA_CLI_VERSION%" diff --git a/dist/bin/cli-common b/dist/bin/cli-common deleted file mode 100644 index d295d58916da..000000000000 --- a/dist/bin/cli-common +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env bash - -#/*-------------------------------------------------------------------------- -# * Credits: This script is based on the script generated by sbt-pack. -# *--------------------------------------------------------------------------*/ - -# save terminal settings -saved_stty=$(stty -g 2>/dev/null) -# clear on error so we don't later try to restore them -if [[ ! $? ]]; then - saved_stty="" -fi - -# restore stty settings (echo in particular) -function restoreSttySettings() { - stty $saved_stty - saved_stty="" -} - -scala_exit_status=127 -function onExit() { - [[ "$saved_stty" != "" ]] && restoreSttySettings - exit $scala_exit_status -} - -#/*-------------------------------------------------------------------------- -# * SECTION FOR JAVA COMMAND -# *--------------------------------------------------------------------------*/ - -# to reenable echo if we are interrupted before completing. -trap onExit INT TERM EXIT - -unset cygwin mingw msys darwin conemu - -# COLUMNS is used together with command line option '-pageWidth'. -if command -v tput >/dev/null 2>&1; then - export COLUMNS="$(tput -Tdumb cols)" -fi - -case "`uname`" in - CYGWIN*) cygwin=true - ;; - MINGW*) mingw=true - ;; - MSYS*) msys=true - ;; - Darwin*) darwin=true - if [ -z "$JAVA_VERSION" ] ; then - JAVA_VERSION="CurrentJDK" - else - echo "Using Java version: $JAVA_VERSION" 1>&2 - fi - if [ -z "$JAVA_HOME" ] ; then - JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/${JAVA_VERSION}/Home - fi - JAVACMD="`which java`" - ;; -esac - -unset CYGPATHCMD -if [[ ${cygwin-} || ${mingw-} || ${msys-} ]]; then - # ConEmu terminal is incompatible with jna-5.*.jar - [[ (${CONEMUANSI-} || ${ConEmuANSI-}) ]] && conemu=true - # cygpath is used by various windows shells: cygwin, git-sdk, gitbash, msys, etc. - CYGPATHCMD=`which cygpath 2>/dev/null` - case "$TERM" in - rxvt* | xterm* | cygwin*) - stty -icanon min 1 -echo - JAVA_OPTS="$JAVA_OPTS -Djline.terminal=unix" - ;; - esac -fi - -# Resolve JAVA_HOME from javac command path -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" -a -f "$javaExecutable" -a ! "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - javaExecutable="`readlink -f \"$javaExecutable\"`" - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "${JAVACMD-}" ] ; then - if [ -n "${JAVA_HOME-}" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." - echo " We cannot execute $JAVACMD" - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSPATH_SUFFIX="" -# Path separator used in EXTRA_CLASSPATH -PSEP=":" - -# translate paths to Windows-mixed format before running java -if [ -n "${CYGPATHCMD-}" ]; then - [ -n "${PROG_HOME-}" ] && - PROG_HOME=`"$CYGPATHCMD" -am "$PROG_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`"$CYGPATHCMD" -am "$JAVA_HOME"` - CLASSPATH_SUFFIX=";" - PSEP=";" -elif [[ ${mingw-} || ${msys-} ]]; then - # For Mingw / Msys, convert paths from UNIX format before anything is touched - [ -n "$PROG_HOME" ] && - PROG_HOME="`(cd "$PROG_HOME"; pwd -W | sed 's|/|\\\\|g')`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd -W | sed 's|/|\\\\|g')`" - CLASSPATH_SUFFIX=";" - PSEP=";" -fi - -#/*-------------------------------------------------- -# * The code below is for Dotty -# *-------------------------------------------------*/ - -find_lib () { - for lib in "$PROG_HOME"/lib/$1 ; do - if [[ -f "$lib" ]]; then - if [ -n "$CYGPATHCMD" ]; then - "$CYGPATHCMD" -am "$lib" - elif [[ $mingw || $msys ]]; then - echo "$lib" | sed 's|/|\\\\|g' - else - echo "$lib" - fi - return - fi - done -} - -SCALA_CLI_JAR="$PROG_HOME/etc/scala-cli.jar" - -declare -a scala_args - -addScala () { - scala_args+=("'$1'") -} diff --git a/dist/bin/cli-common-platform b/dist/bin/cli-common-platform deleted file mode 100644 index a5906e882bb4..000000000000 --- a/dist/bin/cli-common-platform +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -SCALA_CLI_CMD_BASH=("\"$JAVACMD\"" "-jar \"$PROG_HOME/bin/scala-cli.jar\"") diff --git a/dist/bin/cli-common-platform.bat b/dist/bin/cli-common-platform.bat deleted file mode 100644 index 99103266c1d9..000000000000 --- a/dist/bin/cli-common-platform.bat +++ /dev/null @@ -1,5 +0,0 @@ -@echo off - -@rem we need to escape % in the java command path, for some reason this doesnt work in common.bat -set "_JAVACMD=!_JAVACMD:%%=%%%%!" -set SCALA_CLI_CMD_WIN="%_JAVACMD%" "-jar" "%_PROG_HOME%\bin\scala-cli.jar" \ No newline at end of file diff --git a/dist/bin/common-shared b/dist/bin/common-shared deleted file mode 100644 index 8c85993a5283..000000000000 --- a/dist/bin/common-shared +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env bash - -# Common options for both scala-cli and java based launchers - -#/*-------------------------------------------------------------------------- -# * Credits: This script is based on the script generated by sbt-pack. -# *--------------------------------------------------------------------------*/ - -# save terminal settings -saved_stty=$(stty -g 2>/dev/null) -# clear on error so we don't later try to restore them -if [[ ! $? ]]; then - saved_stty="" -fi - -# restore stty settings (echo in particular) -function restoreSttySettings() { - stty $saved_stty - saved_stty="" -} - -scala_exit_status=127 -function onExit() { - [[ "$saved_stty" != "" ]] && restoreSttySettings - exit $scala_exit_status -} - -# to reenable echo if we are interrupted before completing. -trap onExit INT TERM EXIT - -unset cygwin mingw msys darwin conemu - -# COLUMNS is used together with command line option '-pageWidth'. -if command -v tput >/dev/null 2>&1; then - export COLUMNS="$(tput -Tdumb cols)" -fi - -case "`uname`" in - CYGWIN*) cygwin=true - ;; - MINGW*) mingw=true - ;; - MSYS*) msys=true - ;; - Darwin*) darwin=true - if [ -z "$JAVA_VERSION" ] ; then - JAVA_VERSION="CurrentJDK" - else - echo "Using Java version: $JAVA_VERSION" 1>&2 - fi - if [ -z "$JAVA_HOME" ] ; then - JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/${JAVA_VERSION}/Home - fi - JAVACMD="`which java`" - ;; -esac - -unset CYGPATHCMD -if [[ ${cygwin-} || ${mingw-} || ${msys-} ]]; then - # ConEmu terminal is incompatible with jna-5.*.jar - [[ (${CONEMUANSI-} || ${ConEmuANSI-}) ]] && conemu=true - # cygpath is used by various windows shells: cygwin, git-sdk, gitbash, msys, etc. - CYGPATHCMD=`which cygpath 2>/dev/null` - case "$TERM" in - rxvt* | xterm* | cygwin*) - stty -icanon min 1 -echo - JAVA_OPTS="$JAVA_OPTS -Djline.terminal=unix" - ;; - esac -fi - -# Resolve JAVA_HOME from javac command path -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" -a -f "$javaExecutable" -a ! "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - javaExecutable="`readlink -f \"$javaExecutable\"`" - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "${JAVACMD-}" ] ; then - if [ -n "${JAVA_HOME-}" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." - echo " We cannot execute $JAVACMD" - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSPATH_SUFFIX="" -# Path separator used in EXTRA_CLASSPATH -PSEP=":" -PROG_HOME_URI="file://$PROG_HOME" - -# translate paths to Windows-mixed format before running java -if [ -n "${CYGPATHCMD-}" ]; then - [ -n "${PROG_HOME-}" ] && - PROG_HOME=`"$CYGPATHCMD" -am "$PROG_HOME"` - PROG_HOME_URI="file:///$PROG_HOME" # Add extra root dir prefix - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`"$CYGPATHCMD" -am "$JAVA_HOME"` - CLASSPATH_SUFFIX=";" - PSEP=";" -elif [[ ${mingw-} || ${msys-} ]]; then - # For Mingw / Msys, convert paths from UNIX format before anything is touched - [ -n "$PROG_HOME" ] && - PROG_HOME="`(cd "$PROG_HOME"; pwd -W | sed 's|/|\\\\|g')`" - PROG_HOME_URI="file:///$PROG_HOME" # Add extra root dir prefix - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd -W | sed 's|/|\\\\|g')`" - CLASSPATH_SUFFIX=";" - PSEP=";" -fi - -declare -a scala_args -addScala () { - scala_args+=("'$1'") -} diff --git a/docs/_docs/contributing/setting-up-your-ide.md b/docs/_docs/contributing/setting-up-your-ide.md index 3779ce1c3403..690b42a37a51 100644 --- a/docs/_docs/contributing/setting-up-your-ide.md +++ b/docs/_docs/contributing/setting-up-your-ide.md @@ -42,7 +42,15 @@ want to make sure you do two things: + val enableBspAllProjects = true, ``` -2. Run `sbt publishLocal` to get the needed presentation compiler jars. +2. Run in sbt shell `sbt> scala3-bootstrapped/compile` and then `sbt> scala3-bootstrapped/publishLocalBin` + to get the required presentation compiler jars. + + If any step fails due to random errors, try removing `./out/` directory and running `sbt> clean` + + This step has to be repeated every time compiler version has been bumped. + + + By default Metals uses Bloop build server, however you can also use sbt directly. You can achieve this with the `Metals: Switch Build Server` command diff --git a/docs/_docs/internals/exclusive-capabilities.md b/docs/_docs/internals/exclusive-capabilities.md new file mode 100644 index 000000000000..97c6592ac693 --- /dev/null +++ b/docs/_docs/internals/exclusive-capabilities.md @@ -0,0 +1,551 @@ +# Exclusive Capabilities + +Language design draft + + +## Capability Kinds + +A capability is called + - _exclusive_ if it is `cap` or it has an exclusive capability in its capture set. + - _shared_ otherwise. + +There is a new top capability `shared` which can be used as a capability for deriving shared capture sets. Other shared capabilities are created as read-only versions of exclusive capabilities. + +## Update Methods + +We introduce a new trait +```scala +trait Mutable +``` +It is used as a base trait for types that define _update methods_ using +a new modifier `mut`. + +`mut` can only be used in classes or objects extending `Mutable`. An update method is allowed to access exclusive capabilities in the method's environment. By contrast, a normal method in a type extending `Mutable` may access exclusive capabilities only if they are defined locally or passed to it in parameters. + +**Example:** +```scala +class Ref(init: Int) extends Mutable: + private var current = init + def get: Int = current + mut def put(x: Int): Unit = current = x +``` +Here, `put` needs to be declared as an update method since it accesses the exclusive write capability of the variable `current` in its environment. +`mut` can also be used on an inner class of a class or object extending `Mutable`. It gives all code in the class the right +to access exclusive capabilities in the class environment. Normal classes +can only access exclusive capabilities defined in the class or passed to it in parameters. + +```scala +object Registry extends Mutable: + var count = 0 + mut class Counter: + mut def next: Int = + count += 1 + count +``` +Normal method members of `Mutable` classes cannot call update methods. This is indicated since accesses in the callee are recorded in the caller. So if the callee captures exclusive capabilities so does the caller. + +An update method cannot implement or override a normal method, whereas normal methods may implement or override update methods. Since methods such as `toString` or `==` inherited from Object are normal methods, it follows that none of these methods may be implemented as an update method. + +The `apply` method of a function type is also a normal method, hence `Mutable` classes may not implement a function type with an update method as the `apply` method. + +## Mutable Types + +A type is called a _mutable_ if it extends `Mutable` and it has an update method or an update class as non-private member or constructor. + +When we create an instance of a mutable type we always add `cap` to its capture set. For instance, if class `Ref` is declared as shown previously then `new Ref(1)` has type `Ref[Int]^{cap}`. + +**Restriction:** A non-mutable type cannot be downcast by a pattern match to a mutable type. + +**Definition:** A class is _read_only_ if the following conditions are met: + + 1. It does not extend any exclusive capabilities from its environment. + 2. It does not take parameters with exclusive capabilities. + 3. It does not contain mutable fields, or fields that take exclusive capabilities. + +**Restriction:** If a class or trait extends `Mutable` all its parent classes or traits must either extend `Mutable` or be read-only. + +The idea is that when we upcast a reference to a type extending `Mutable` to a type that does not extend `Mutable`, we cannot possibly call a method on this reference that uses an exclusive capability. Indeed, by the previous restriction this class must be a read-only class, which means that none of the code implemented +in the class can access exclusive capabilities on its own. And we +also cannot override any of the methods of this class with a method +accessing exclusive capabilities, since such a method would have +to be an update method and update methods are not allowed to override regular methods. + + + +**Example:** + +Consider trait `IterableOnce` from the standard library. + +```scala +trait IterableOnce[+T] extends Mutable: + def iterator: Iterator[T]^{this} + mut def foreach(op: T => Unit): Unit + mut def exists(op: T => Boolean): Boolean + ... +``` +The trait is a mutable type with many update methods, among them `foreach` and `exists`. These need to be classified as `mut` because their implementation in the subtrait `Iterator` uses the update method `next`. +```scala +trait Iterator[T] extends IterableOnce[T]: + def iterator = this + def hasNext: Boolean + mut def next(): T + mut def foreach(op: T => Unit): Unit = ... + mut def exists(op; T => Boolean): Boolean = ... + ... +``` +But there are other implementations of `IterableOnce` that are not mutable types (even though they do indirectly extend the `Mutable` trait). Notably, collection classes implement `IterableOnce` by creating a fresh +`iterator` each time one is required. The mutation via `next()` is then restricted to the state of that iterator, whereas the underlying collection is unaffected. These implementations would implement each `mut` method in `IterableOnce` by a normal method without the `mut` modifier. + +```scala +trait Iterable[T] extends IterableOnce[T]: + def iterator = new Iterator[T] { ... } + def foreach(op: T => Unit) = iterator.foreach(op) + def exists(op: T => Boolean) = iterator.exists(op) +``` +Here, `Iterable` is not a mutable type since it has no update method as member. +All inherited update methods are (re-)implemented by normal methods. + +**Note:** One might think that we don't need a base trait `Mutable` since in any case +a mutable type is defined by the presence of update methods, not by what it extends. In fact the importance of `Mutable` is that it defines _the other methods_ as read-only methods that _cannot_ access exclusive capabilities. For types not extending `Mutable`, this is not the case. For instance, the `apply` method of a function type is not an update method and the type itself does not extend `Mutable`. But `apply` may well be implemented by +a method that accesses exclusive capabilities. + + + +## Read-only Capabilities + +If `x` is an exclusive capability of a type extending `Mutable`, `x.rd` is its associated, shared _read-only_ capability. + +`shared` can be understood as the read-only capability corresponding to `cap`. +```scala + shared = cap.rd +``` + +A _top capability_ is either `cap` or `shared`. + + +## Shorthands + +**Meaning of `^`:** + +The meaning of `^` and `=>` is the same as before: + + - `C^` means `C^{cap}`. + - `A => B` means `(A -> B)^{cap}`. + +**Implicitly added capture sets** + +A reference to a type extending any of the traits `Capability` or `Mutable` gets an implicit capture set `{shared}` in case no explicit capture set is given. + +For instance, a matrix multiplication method can be expressed as follows: + +```scala +class Matrix(nrows: Int, ncols: Int) extends Mutable: + mut def update(i: Int, j: Int, x: Double): Unit = ... + def apply(i: Int, j: Int): Double = ... + +def mul(a: Matrix, b: Matrix, c: Matrix^): Unit = + // multiply a and b, storing the result in c +``` +Here, `a` and `b` are implicitly read-only, and `c`'s type has capture set `cap`. I.e. with explicit capture sets this would read: +```scala +def mul(a: Matrix^{shared}, b: Matrix^{shared}, c: Matrix^{cap}): Unit +``` +Separation checking will then make sure that `a` and `b` must be different from `c`. + + +## Capture Sets + +As the previous example showed, we would like to use a `Mutable` type such as `Array` or `Matrix` in two permission levels: read-only and unrestricted. A standard technique is to invent a type qualifier such as "read-only" or "mutable" to indicate access permissions. What we would like to do instead is to combine the qualifier with the capture set of a type. So we +distinguish two kinds of capture sets: regular and read-only. Read-only sets can contain only shared capabilities. + +Internally, in the discussion that follows we use a label after the set to indicate its mode. `{...}_` is regular and `{...}rd` is read-only. We could envisage source language to specify read-only sets, e.g. something like + +```scala +{io, async}.rd +``` + +But in almost all cases we don't need an explicit mode in source code to indicate the kind of capture set, since the contents of the set itself tell us what kind it is. A capture set is assumed to be read-only if it is on a +type extending `Mutable` and it contains only shared capabilities, otherwise it is assumed to be regular. + +The read-only function `ro` maps capture sets to read-only capture sets. It is defined pointwise on capabilities as follows: + + - `ro ({ x1, ..., xn } _) = { ro(x1), ..., ro(xn) }` + - `ro(x) = x` if `x` is shared + - `ro(x) = x.rd` if `x` is exclusive + + + +## Subcapturing + +Subcapturing has to take the mode of capture sets into account. We let `m` stand for arbitrary modes. + +1. Rule (sc-var) comes in two variants. If `x` is defined as `S^C` then + + - `{x, xs} m <: (C u {xs}) m` + - `{x.rd, xs} m <: (ro(C) u {xs}) m` + +3. The subset rule works only between sets of the same kind: + + - `C _ <: C _ u {x}` + - `C rd <: C rd u {x}` if `x` is a shared capability. + +4. We can map regular capture sets to read-only sets: + + - `C _ <: ro(C) rd` + +5. Read-only capabilities in regular capture sets can be widened to exclusive capabilities: + + - `{x.rd, xs} _ <: {x, xs} _` + +One case where an explicit capture set mode would be useful concerns +refinements of type variable bounds, as in the following example. +```scala +class A: + type T <: Object^{x.rd, y} +class B extends A: + type T <: Object^{x.rd} +class C extends B: + type T = Matrix^{x.rd} +``` +We assume that `x` and `y` are exclusive capabilities. +The capture set of type `T` in class `C` is a read-only set since `Matrix` extends `Mutable`. But the capture sets of the occurrences of +`T` in `A` and `B` are regular. This leads to an error in bounds checking +the definition of `T` in `C` against the one in `B` +since read-only sets do not subcapture regular sets. We can fix the +problem by declaring the capture set in class `B` as read-only: +```scala +class B extends A: + type T <: Object^{x.rd}.rd +``` +But now a different problem arises since the capture set of `T` in `B` is +read-only but the capture set of `T` and `A` is regular. The capture set of +`T` in `A` cannot be made read-only since it contains an exclusive capability `y`. So we'd have to drop `y` and declare class `A` like this: +```scala +class A: + type T <: Object^{x.rd}.rd +``` + + + +## Accesses to Mutable Types + +A _read-only access_ is a reference `x` to a type extending `Mutable` with a regular capture set if the expected type is one of the following: + + - a value type that is not a mutable type, or + - a select prototype with a member that is a normal method or class (not an update method or class). + +A read-only access contributes the read-only capability `x.rd` to its environment (as formalized by _cv_). Other accesses contribute the full capability `x`. + +A reference `p.m` to an update method or class `m` of a mutable type is allowed only if `p`'s capture set is regular. + +If `e` is an expression of a type `T^cs` extending `Mutable` and the expected type is a value type that is not a mutable type, then the type of `e` is mapped to `T^ro(cs)`. + + +## Expression Typing + +An expression's type should never contain a top capability in its deep capture set. This is achieved by the following rules: + + - On var access `x`: + + - replace all direct capture sets with `x` + - replace all boxed caps with `x*` + + _Variant_: If the type of the typevar corresponding to a boxed cap can be uniquely reached by a path `this.p`, replace the `cap` with `x.p*`. + + - On select `t.foo` where `C` is the capture set of `t`: apply the SELECT rule, which amounts to: + + - replace all direct caps with `C` + - replace all boxed caps with `C*` + + - On applications: `t(args)`, `new C(args)` if the result type `T` contains `cap` (deeply): + + - create a fresh skolem `val sk: T` + - set result type to `sk.type` + + Skolem symbols are eliminated before they reach the type of the enclosing val or def. + + - When avoiding a variable in a local block, as in: + ```scala + { val x: T^ = ...; ... r: List[T^{x}] } + ``` + where the capture set of `x` contains a top capability, + replace `x` by a fresh skolem `val sk: T`. Alternatively: keep it as is, but don't widen it. + + +## Post Processing Right Hand Sides + +The type of the right hand sides of `val`s or `def`s is post-processed before it becomes the inferred type or is compared with the declared type. Post processing +means that all local skolems in the type are avoided, which might mean `cap` can now occur in the the type. + +However, if a local skolem `sk` has `cap` as underlying type, but is only used +in its read-only form `sk.rd` in the result type, we can drop the skolem instead of widening to `shared`. + +**Example:** + +```scala + def f(x: Int): Double = ... + + def precomputed(n: Int)(f: Int -> Double): Int -> Double = + val a: Array[Double]^ = Array.tabulate(n)(f) + a(_) +``` +Here, `Array.tabulate(n)(f)` returns a value of type `Array[Double]^{cap}`. +The last expression `a(_)` expands to the closure `idx => a(idx)`, which +has type `Int ->{a.rd} Double`, since `a` appears only in the context of a +selection with the `apply` method of `Array`, which is not an update method. The type of the enclosing block then has type `Int ->{sk.rd} Double` for a fresh skolem `sk`, +since `a` is no longer visible. After post processing, this type becomes +`Int -> Double`. + +This pattern allows to use mutation in the construction of a local data structure, returning a pure result when the construction is done. Such +data structures are said to have _transient mutability_. + +## Separation checking + +Separation checking checks that we don't have hidden aliases. A hidden alias arises when we have two definitions `x` and `y` with overlapping transitive capture sets that are not manifest in the types of `x` and `y` because one of these types has widened the alias to a top capability. + +Since expression types can't mention cap, widening happens only + - when passing an argument to a parameter + - when widening to a declared (result) type of a val or def + +**Definitions:** + + - The _transitive capture set_ `tcs(c)` of a capability `c` with underlying capture set `C` is `c` itself, plus the transitive capture set of `C`, but excluding `cap` or `shared`. + + - The _transitive capture set_ `tcs(C)` of a capture set C is the union + of `tcs(c)` for all elements `c` of `C`. + + - Two capture sets _interfere_ if one contains an exclusive capability `x` and the other + also contains `x` or contains the read-only capability `x.rd`. + + - If `C1 <: C2` and `C2` contains a top capability, then let `C2a` be `C2` without top capabilities. The hidden set `hidden(C1, C2)` of `C1` relative to `C2` is the smallest subset `C1h` of `C1` such that `C1 \ C1h <: C2a`. + + - If `T1 <: T2` then let the hidden set `hidden(T1, T2)` of `T1` relative to `T2` be the + union of all hidden sets of corresponding capture sets in `T1` and `T2`. + + +**Algorithm outline:** + + - Associate _shadowed sets_ with blocks, template statement sequences, applications, and val symbols. The idea is that a shadowed set gets populated when a capture reference is widened to cap. In that case the original references that were widened get added to the set. + + - After processing a `val x: T2 = t` with `t: T1` after post-processing: + + - If `T2` is declared, add `tcs(hidden(T1, T2))` to the shadowed set + of the enclosing statement sequence and remember it as `shadowed(x)`. + - If`T2` is inferred, add `tcs(T1)` to the shadowed set + of the enclosing statement sequence and remember it as `shadowed(x)`. + + - When processing the right hand side of a `def f(params): T2 = t` with `t: T1` after post-processing + + - If `T2` is declared, check that `shadowed*(hidden(T1, T2))` contains only local values (including skolems). + - If `T2` is inferred, check that `shadowed*(tcs(T1))` contains only local values (including skolems). + + Here, `shadowed*` is the transitive closure of `shadowed`. + + - When processing an application `p.f(arg1, ..., arg_n)`, after processing `p`, add its transitive capture set to the shadowed set of the call. Then, in sequence, process each argument by adding `tcs(hidden(T1, T2))` to the shadowed set of the call, where `T1` is the argument type and `T2` is the type of the formal parameter. + + - When adding a reference `r` or capture set `C` in `markFree` to enclosing environments, check that `tcs(r)` (respectively, `tcs(C)`) does not interfere with an enclosing shadowed set. + + +This requires, first, a linear processing of the program in evaluation order, and, second, that all capture sets are known. Normal rechecking violates both of these requirements. First, definitions +without declared result types are lazily rechecked using completers. Second, capture sets are constructed +incrementally. So we probably need a second scan after rechecking proper. In order not to duplicate work, we need to record during rechecking all additions to environments via `markFree`. + +**Notes:** + + - Mutable variables are not allowed to have top capabilities in their deep capture sets, so separation checking is not needed for checking var definitions or assignments. + + - A lazy val can be thought of conceptually as a value with possibly a capturing type and as a method computing that value. A reference to a lazy val is interpreted as a call to that method. It's use set is the reference to the lazy val itself as well as the use set of the called method. + + - + +## Escape Checking + +The rules for separation checking also check that capabilities do not escape. Separate +rules for explicitly preventing cap to be boxed or unboxed are not needed anymore. Consider the canonical `withFile` example: +```scala +def withFile[T](body: File^ => T): T = + ... + +withFile: f => + () => f.write("too late") +``` +Here, the argument to `withFile` has the dependent function type +```scala +(f: File^) -> () ->{f} Unit +``` +A non-dependent type is required so the expected result type of the closure is +``` +() ->{cap} Unit +``` +When typing a closure, we type an anonymous function. The result type of that function is determined by type inference. That means the generated closure looks like this +```scala +{ def $anon(f: File^): () ->{cap} Unit = + () => f.write("too late") + $anon +} +``` +By the rules of separation checking the hidden set of the body of $anon is `f`, which refers +to a value outside the rhs of `$anon`. This is illegal according to separation checking. + +In the last example, `f: File^` was an exclusive capability. But it could equally have been a shared capability, i.e. `withFile` could be formulated as follows: +```scala +def withFile[T](body: File^{shared} => T): T = +``` +The same reasoning as before would enforce that there are no leaks. + + +## Mutable Variables + +Local mutable variables are tracked by default. It is essentially as if a mutable variable `x` was decomposed into a new private field of class `Ref` together with a getter and setter. I.e. instead of +```scala +var x: T = init +``` +we'd deal with +```scala +val x$ = Ref[T](init) +def x = x$.get +mut def x_=(y: T) = x$.put(y) +``` + +There should be a way to exclude a mutable variable or field from tracking. Maybe an annotation or modifier such as `transparent` or `untracked`? + +The expansion outlined above justifies the following rules for handling mutable variables directly: + + - A type with non-private tracked mutable fields is classified as mutable. + It has to extend the `Mutable` class. + - A read access to a local mutable variable `x` charges the capability `x.rd` to the environment. + - An assignment to a local mutable variable `x` charges the capability `x` to the environment. + - A read access to a mutable field `this.x` charges the capability `this.rd` to the environment. + - A write access to a mutable field `this.x` charges the capability `this` to the environment. + +Mutable Scopes +============== + +We sometimes want to make separation checking coarser. For instance when constructing a doubly linked list we want to create `Mutable` objects and +store them in mutable variables. Since a variable's type cannot contain `cap`, +we must know beforehand what mutable objects it can be refer to. This is impossible if the other objects are created later. + +Mutable scopes provide a solution to this they permit to derive a set of variables from a common exclusive reference. We define a new class +```scala +class MutableScope extends Mutable +``` +To make mutable scopes useful, we need a small tweak +of the rule governing `new` in the _Mutable Types_ section. The previous rule was: + +> When we create an instance of a mutable type we always add `cap` to its capture set. + +The new rule is: + +> When we create an instance of a mutable type we search for a given value of type `MutableScope`. If such a value is found (say it is `ms`) then we use +`ms` as the capture set of the created instance. Otherwise we use `cap`. + +We could envisage using mutable scopes like this: +``` +object enclave: + private given ms: MutableScope() + + ... +``` +Within `enclave` all mutable objects have `ms` as their capture set. So they can contain variables that also have `ms` as their capture set of their values. + +Mutable scopes should count as mutable types (this can be done either by decree or by adding an update method to `MutableScope`). Hence, mutable scopes can themselves be nested inside other mutable scopes. + +## Consumed Capabilities + +We allow `consume` as a modifier on parameters and methods. Example: + +```scala +class C extends Capability + +class Channel[T]: + def send(consume x: T) + + + +class Buffer[+T] extends Mutable: + consume def append(x: T): Buffer[T]^ + +b.append(x) +b1.append(y) + +def concat[T](consume buf1: Buffer[T]^, buf2: Buffer[T]): Buffer[T]^ + +A ->{x.consume} B + + +A + + C , Gamma, x: S |- t; T + --------------------------- + , Gamma |- (x -> t): S ->C T + + + C, Gamma |- let x = s in t: T + + +class Iterator[T]: + consume def filter(p: T => Boolean): Iterator[T]^ + consume def exists(p: T => Boolean): Boolean +``` + +As a parameter, `consume` implies `^` as capture set of the parameter type. The `^` can be given, but is redundant. + +When a method with a `consume` parameter of type `T2^` is called with an argument of type `T1`, we add the elements of `tcs(hidden(T1, T2^))` not just to the enclosing shadowed set but to all enclosing shadowed sets where elements are visible. This makes these elements permanently inaccessible. + + + +val f = Future { ... } +val g = Future { ... } + + +A parameter is implicitly @unbox if it contains a boxed cap. Example: + +def apply[T](f: Box[T => T], y: T): T = + xs.head(y) + +def compose[T](fs: @unbox List[T => T]) = + xs.foldRight(identity)((f: T => T, g: T => T) => x => g(f(x))) + + + +compose(List(f, g)) + +f :: g :: Nil + +def compose[T](fs: List[Unbox[T => T]], x: T) = + val combined = (xs.foldRight(identity)((f: T => T, g: T => T) => x => g(f(x)))): T->{fs*} T + combined(x) + + +With explicit diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index 665b4f5144ba..6c144f436690 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -387,8 +387,8 @@ DefParamClauses ::= DefParamClause { DefParamClause } -- and two DefTypeParam DefParamClause ::= DefTypeParamClause | DefTermParamClause | UsingParamClause -TypelessClauses ::= TypelessClause {TypelessClause} -TypelessClause ::= DefTermParamClause +ConstrParamClauses::= ConstrParamClause {ConstrParamClause} +ConstrParamClause ::= DefTermParamClause | UsingParamClause DefTermParamClause::= [nl] ‘(’ [DefTermParams] ‘)’ UsingParamClause ::= [nl] ‘(’ ‘using’ (DefTermParams | FunArgTypes) ‘)’ @@ -459,7 +459,7 @@ Def ::= ‘val’ PatDef PatDef ::= ids [‘:’ Type] [‘=’ Expr] | Pattern2 [‘:’ Type] [‘=’ Expr] PatDef(_, pats, tpe?, expr) DefDef ::= DefSig [‘:’ Type] [‘=’ Expr] DefDef(_, name, paramss, tpe, expr) - | ‘this’ TypelessClauses [DefImplicitClause] ‘=’ ConstrExpr DefDef(_, , vparamss, EmptyTree, expr | Block) + | ‘this’ ConstrParamClauses [DefImplicitClause] ‘=’ ConstrExpr DefDef(_, , vparamss, EmptyTree, expr | Block) DefSig ::= id [DefParamClauses] [DefImplicitClause] TypeDef ::= id [HkTypeParamClause] {FunParamClause} TypeAndCtxBounds TypeDefTree(_, name, tparams, bound [‘=’ Type] diff --git a/docs/_docs/reference/dropped-features/type-projection.md b/docs/_docs/reference/dropped-features/type-projection.md index 2c3e82ce99b8..9b9f643ceb6e 100644 --- a/docs/_docs/reference/dropped-features/type-projection.md +++ b/docs/_docs/reference/dropped-features/type-projection.md @@ -4,15 +4,18 @@ title: "Dropped: General Type Projection" nightlyOf: https://docs.scala-lang.org/scala3/reference/dropped-features/type-projection.html --- -Scala so far allowed general type projection `T#A` where `T` is an arbitrary type -and `A` names a type member of `T`. +Scala 2 allowed general type projection `T#A` where `T` is an arbitrary type and `A` names a type member of `T`. +This turns out to be [unsound](https://github.com/scala/scala3/issues/1050) (at least when combined with other Scala 3 features). -Scala 3 disallows this if `T` is an abstract type (class types and type aliases -are fine). This change was made because unrestricted type projection -is [unsound](https://github.com/scala/scala3/issues/1050). - -This restriction rules out the [type-level encoding of a combinator -calculus](https://michid.wordpress.com/2010/01/29/scala-type-level-encoding-of-the-ski-calculus/). +To remedy this, Scala 3 only allows type projection if `T` is a concrete type (any type which is not abstract), an example for such a type would be a class type (`class T`). +A type is abstract if it is: +* An abstract type member (`type T` without `= SomeType`) +* A type parameter (`[T]`) +* An alias to an abstract type (`type T = SomeAbstractType`). +There are no restriction on `A` apart from the fact it has to be a member type of `T`, for example a subclass (`class T { class A }`). To rewrite code using type projections on abstract types, consider using path-dependent types or implicit parameters. + +This restriction rules out the [type-level encoding of a combinator +calculus](https://michid.wordpress.com/2010/01/29/scala-type-level-encoding-of-the-ski-calculus/). \ No newline at end of file diff --git a/docs/_docs/reference/other-new-features/type-test.md b/docs/_docs/reference/other-new-features/type-test.md index ec7a87230753..fb2a2e584711 100644 --- a/docs/_docs/reference/other-new-features/type-test.md +++ b/docs/_docs/reference/other-new-features/type-test.md @@ -63,7 +63,7 @@ We could create a type test at call site where the type test can be performed wi val tt: TypeTest[Any, String] = new TypeTest[Any, String]: def unapply(s: Any): Option[s.type & String] = s match - case s: String => Some(s) + case q: (s.type & String) => Some(q) case _ => None f[AnyRef, String]("acb")(using tt) diff --git a/docs/_docs/reference/syntax.md b/docs/_docs/reference/syntax.md index 0f78ff03583e..ccba2ec9578a 100644 --- a/docs/_docs/reference/syntax.md +++ b/docs/_docs/reference/syntax.md @@ -363,8 +363,8 @@ DefParamClauses ::= DefParamClause { DefParamClause } -- and two DefTypeParam DefParamClause ::= DefTypeParamClause | DefTermParamClause | UsingParamClause -TypelessClauses ::= TypelessClause {TypelessClause} -TypelessClause ::= DefTermParamClause +ConstrParamClauses::= ConstrParamClause {ConstrParamClause} +ConstrParamClause ::= DefTermParamClause | UsingParamClause DefTermParamClause::= [nl] ‘(’ [DefTermParams] ‘)’ @@ -433,7 +433,7 @@ Def ::= ‘val’ PatDef PatDef ::= ids [‘:’ Type] [‘=’ Expr] | Pattern2 [‘:’ Type] [‘=’ Expr] PatDef(_, pats, tpe?, expr) DefDef ::= DefSig [‘:’ Type] [‘=’ Expr] DefDef(_, name, paramss, tpe, expr) - | ‘this’ TypelessClauses [DefImplicitClause] ‘=’ ConstrExpr DefDef(_, , vparamss, EmptyTree, expr | Block) + | ‘this’ ConstrParamClauses [DefImplicitClause] ‘=’ ConstrExpr DefDef(_, , vparamss, EmptyTree, expr | Block) DefSig ::= id [DefParamClauses] [DefImplicitClause] TypeDef ::= id [HkTypeParamClause] {FunParamClause}TypeBounds TypeDefTree(_, name, tparams, bound [‘=’ Type] diff --git a/docs/_spec/TODOreference/other-new-features/type-test.md b/docs/_spec/TODOreference/other-new-features/type-test.md index ec7a87230753..fb2a2e584711 100644 --- a/docs/_spec/TODOreference/other-new-features/type-test.md +++ b/docs/_spec/TODOreference/other-new-features/type-test.md @@ -63,7 +63,7 @@ We could create a type test at call site where the type test can be performed wi val tt: TypeTest[Any, String] = new TypeTest[Any, String]: def unapply(s: Any): Option[s.type & String] = s match - case s: String => Some(s) + case q: (s.type & String) => Some(q) case _ => None f[AnyRef, String]("acb")(using tt) diff --git a/library/src/scala/CanThrow.scala b/library/src/scala/CanThrow.scala index 91c94229c43c..485dcecb37df 100644 --- a/library/src/scala/CanThrow.scala +++ b/library/src/scala/CanThrow.scala @@ -8,7 +8,7 @@ import annotation.{implicitNotFound, experimental, capability} */ @experimental @implicitNotFound("The capability to throw exception ${E} is missing.\nThe capability can be provided by one of the following:\n - Adding a using clause `(using CanThrow[${E}])` to the definition of the enclosing method\n - Adding `throws ${E}` clause after the result type of the enclosing method\n - Wrapping this piece of code with a `try` block that catches ${E}") -erased class CanThrow[-E <: Exception] extends caps.Capability +erased class CanThrow[-E <: Exception] extends caps.SharedCapability @experimental object unsafeExceptions: diff --git a/library/src/scala/annotation/internal/readOnlyCapability.scala b/library/src/scala/annotation/internal/readOnlyCapability.scala new file mode 100644 index 000000000000..8e939aea6bb9 --- /dev/null +++ b/library/src/scala/annotation/internal/readOnlyCapability.scala @@ -0,0 +1,7 @@ +package scala.annotation +package internal + +/** An annotation that marks a capture ref as a read-only capability. + * `x.rd` is encoded as `x.type @readOnlyCapability` + */ +class readOnlyCapability extends StaticAnnotation diff --git a/library/src/scala/annotation/retains.scala b/library/src/scala/annotation/retains.scala index 909adc13a1c2..9c4af7f2336d 100644 --- a/library/src/scala/annotation/retains.scala +++ b/library/src/scala/annotation/retains.scala @@ -1,12 +1,12 @@ package scala.annotation -/** An annotation that indicates capture of a set of references under -Ycc. +/** An annotation that indicates capture of a set of references under capture checking. * * T @retains(x, y, z) * * is the internal representation used for the capturing type * - * {x, y, z} T + * T ^ {x, y, z} * * The annotation can also be written explicitly if one wants to avoid the * non-standard capturing type syntax. diff --git a/library/src/scala/caps/package.scala b/library/src/scala/caps/package.scala index 3705a6137be8..7f8b184c1c95 100644 --- a/library/src/scala/caps/package.scala +++ b/library/src/scala/caps/package.scala @@ -29,6 +29,16 @@ trait Capability extends Any @experimental object cap extends Capability +/** Marker trait for classes with methods that requires an exclusive reference. */ +@experimental +trait Mutable extends Capability + +/** Marker trait for capabilities that can be safely shared in a concurrent context. + * During separation checking, shared capabilities are not taken into account. + */ +@experimental +trait SharedCapability extends Capability + /** Carrier trait for capture set type parameters */ @experimental trait CapSet extends Any @@ -58,11 +68,21 @@ object Contains: @experimental final class use extends annotation.StaticAnnotation -/** A trait to allow expressing existential types such as - * - * (x: Exists) => A ->{x} B +/** An annotations on parameters and update methods. + * On a parameter it states that any capabilties passed in the argument + * are no longer available afterwards, unless they are of class `SharableCapabilitty`. + * On an update method, it states that the `this` of the enclosing class is + * consumed, which means that any capabilities of the method prefix are + * no longer available afterwards. */ @experimental +final class consume extends annotation.StaticAnnotation + +/** A trait that used to allow expressing existential types. Replaced by +* root.Result instances. +*/ +@experimental +@deprecated sealed trait Exists extends Capability @experimental @@ -80,6 +100,28 @@ object internal: */ extension (x: Any) def reachCapability: Any = x + /** Read-only capabilities x.rd which appear as terms in @retains annotations are encoded + * as `caps.readOnlyCapability(x)`. When converted to CaptureRef types in capture sets + * they are represented as `x.type @annotation.internal.readOnlyCapability`. + */ + extension (x: Any) def readOnlyCapability: Any = x + + /** An internal annotation placed on a refinement created by capture checking. + * Refinements with this annotation unconditionally override any + * info from the parent type, so no intersection needs to be formed. + * This could be useful for tracked parameters as well. + */ + final class refineOverride extends annotation.StaticAnnotation + + /** An annotation used internally for root capability wrappers of `cap` that + * represent either Fresh or Result capabilities. + * A capability is encoded as `caps.cap @rootCapability(...)` where + * `rootCapability(...)` is a special kind of annotation of type `root.Annot` + * that contains either a hidden set for Fresh instances or a method type binder + * for Result instances. + */ + final class rootCapability extends annotation.StaticAnnotation + @experimental object unsafe: /** diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index 8899f734aece..0f5e904e29bb 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -222,6 +222,12 @@ object language: @compileTimeOnly("`future-migration` can only be used at compile time in import statements") object `future-migration` + /** Set source version to 2.13. Effectively, this doesn't change the source language, + * but rather adapts the generated code as if it was compiled with Scala 2.13 + */ + @compileTimeOnly("`2.13` can only be used at compile time in import statements") + private[scala] object `2.13` + /** Set source version to 3.0-migration. * * @see [[https://docs.scala-lang.org/scala3/guides/migration/compatibility-intro.html]] diff --git a/presentation-compiler/src/main/dotty/tools/pc/AutoImports.scala b/presentation-compiler/src/main/dotty/tools/pc/AutoImports.scala index 1b44dce8c642..7b30c745e3ed 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/AutoImports.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/AutoImports.scala @@ -40,7 +40,7 @@ object AutoImports: case class Select(qual: SymbolIdent, name: String) extends SymbolIdent: def value: String = s"${qual.value}.$name" - def direct(name: String): SymbolIdent = Direct(name) + def direct(name: String)(using Context): SymbolIdent = Direct(name) def fullIdent(symbol: Symbol)(using Context): SymbolIdent = val symbols = symbol.ownersIterator.toList @@ -70,7 +70,7 @@ object AutoImports: importSel: Option[ImportSel] ): - def name: String = ident.value + def name(using Context): String = ident.value object SymbolImport: @@ -189,10 +189,13 @@ object AutoImports: ownerImport.importSel, ) else - ( - SymbolIdent.direct(symbol.nameBackticked), - Some(ImportSel.Direct(symbol)), - ) + renames(symbol) match + case Some(rename) => (SymbolIdent.direct(rename), None) + case None => + ( + SymbolIdent.direct(symbol.nameBackticked), + Some(ImportSel.Direct(symbol)), + ) end val SymbolImport( @@ -223,9 +226,13 @@ object AutoImports: importSel ) case None => + val reverse = symbol.ownersIterator.toList.reverse + val fullName = reverse.drop(1).foldLeft(SymbolIdent.direct(reverse.head.nameBackticked)){ + case (acc, sym) => SymbolIdent.Select(acc, sym.nameBackticked(false)) + } SymbolImport( symbol, - SymbolIdent.direct(symbol.fullNameBackticked), + SymbolIdent.Direct(symbol.fullNameBackticked), None ) end match @@ -252,7 +259,6 @@ object AutoImports: val topPadding = if importPosition.padTop then "\n" else "" - val formatted = imports .map { case ImportSel.Direct(sym) => importName(sym) @@ -267,15 +273,16 @@ object AutoImports: end renderImports private def importName(sym: Symbol): String = - if indexedContext.importContext.toplevelClashes(sym) then + if indexedContext.toplevelClashes(sym, inImportScope = true) then s"_root_.${sym.fullNameBackticked(false)}" else sym.ownersIterator.zipWithIndex.foldLeft((List.empty[String], false)) { case ((acc, isDone), (sym, idx)) => if(isDone || sym.isEmptyPackage || sym.isRoot) (acc, true) else indexedContext.rename(sym) match - case Some(renamed) => (renamed :: acc, true) - case None if !sym.isPackageObject => (sym.nameBackticked(false) :: acc, false) - case None => (acc, false) + // we can't import first part + case Some(renamed) if idx != 0 => (renamed :: acc, true) + case _ if !sym.isPackageObject => (sym.nameBackticked(false) :: acc, false) + case _ => (acc, false) }._1.mkString(".") end AutoImportsGenerator diff --git a/presentation-compiler/src/main/dotty/tools/pc/AutoImportsProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/AutoImportsProvider.scala index e35556ad11c9..0a6178eae106 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/AutoImportsProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/AutoImportsProvider.scala @@ -10,6 +10,7 @@ import scala.meta.pc.* import dotty.tools.dotc.ast.tpd.* import dotty.tools.dotc.core.Symbols.* +import dotty.tools.dotc.core.StdNames.* import dotty.tools.dotc.interactive.Interactive import dotty.tools.dotc.interactive.InteractiveDriver import dotty.tools.dotc.util.SourceFile @@ -17,6 +18,7 @@ import dotty.tools.pc.completions.CompletionPos import dotty.tools.pc.utils.InteractiveEnrichments.* import org.eclipse.lsp4j as l +import dotty.tools.dotc.core.Flags.Method final class AutoImportsProvider( search: SymbolSearch, @@ -42,11 +44,22 @@ final class AutoImportsProvider( val path = Interactive.pathTo(newctx.compilationUnit.tpdTree, pos.span)(using newctx) - val indexedContext = IndexedContext( - Interactive.contextOfPath(path)(using newctx) + val indexedContext = IndexedContext(pos)( + using Interactive.contextOfPath(path)(using newctx) ) import indexedContext.ctx + + def correctInTreeContext(sym: Symbol) = path match + case (_: Ident) :: (sel: Select) :: _ => + sym.info.allMembers.exists(_.name == sel.name) + case (_: Ident) :: (_: Apply) :: _ if !sym.is(Method) => + def applyInObject = + sym.companionModule.info.allMembers.exists(_.name == nme.apply) + def applyInClass = sym.info.allMembers.exists(_.name == nme.apply) + applyInClass || applyInObject + case _ => true + val isSeen = mutable.Set.empty[String] val symbols = List.newBuilder[Symbol] def visit(sym: Symbol): Boolean = @@ -83,20 +96,31 @@ final class AutoImportsProvider( text, tree, unit.comments, - indexedContext.importContext, + indexedContext, config ) (sym: Symbol) => generator.forSymbol(sym) end match end mkEdit - for + val all = for sym <- results edits <- mkEdit(sym) - yield AutoImportsResultImpl( + yield (AutoImportsResultImpl( sym.owner.showFullName, edits.asJava - ) + ), sym) + + all match + case (onlyResult, _) :: Nil => List(onlyResult) + case Nil => Nil + case moreResults => + val moreExact = moreResults.filter { case (_, sym) => + correctInTreeContext(sym) + } + if moreExact.nonEmpty then moreExact.map(_._1) + else moreResults.map(_._1) + else List.empty end if end autoImports diff --git a/presentation-compiler/src/main/dotty/tools/pc/ExtractMethodProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/ExtractMethodProvider.scala index c72a0602f1ce..00cde67873d5 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/ExtractMethodProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/ExtractMethodProvider.scala @@ -51,7 +51,7 @@ final class ExtractMethodProvider( given locatedCtx: Context = val newctx = driver.currentCtx.fresh.setCompilationUnit(unit) Interactive.contextOfPath(path)(using newctx) - val indexedCtx = IndexedContext(locatedCtx) + val indexedCtx = IndexedContext(pos)(using locatedCtx) val printer = ShortenedTypePrinter(search, IncludeDefaultParam.Never)(using indexedCtx) def prettyPrint(tpe: Type) = diff --git a/presentation-compiler/src/main/dotty/tools/pc/HoverProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/HoverProvider.scala index 3b2f4d2aa9b0..746be65155d9 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/HoverProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/HoverProvider.scala @@ -49,7 +49,7 @@ object HoverProvider: val path = unit .map(unit => Interactive.pathTo(unit.tpdTree, pos.span)) .getOrElse(Interactive.pathTo(driver.openedTrees(uri), pos)) - val indexedContext = IndexedContext(ctx) + val indexedContext = IndexedContext(pos)(using ctx) def typeFromPath(path: List[Tree]) = if path.isEmpty then NoType else path.head.typeOpt @@ -96,7 +96,7 @@ object HoverProvider: val printerCtx = Interactive.contextOfPath(path) val printer = ShortenedTypePrinter(search, IncludeDefaultParam.Include)( - using IndexedContext(printerCtx) + using IndexedContext(pos)(using printerCtx) ) MetalsInteractive.enclosingSymbolsWithExpressionType( enclosing, @@ -134,7 +134,7 @@ object HoverProvider: .map(_.docstring()) .mkString("\n") - val expresionTypeOpt = + val expresionTypeOpt = if symbol.name == StdNames.nme.??? then InferExpectedType(search, driver, params).infer() else printer.expressionType(exprTpw) diff --git a/presentation-compiler/src/main/dotty/tools/pc/IndexedContext.scala b/presentation-compiler/src/main/dotty/tools/pc/IndexedContext.scala index 7c2c34cf5ebb..2db37f801349 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/IndexedContext.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/IndexedContext.scala @@ -4,64 +4,45 @@ import scala.annotation.tailrec import scala.util.control.NonFatal import dotty.tools.dotc.core.Contexts.* +import dotty.tools.dotc.core.Denotations.PreDenotation +import dotty.tools.dotc.core.Denotations.SingleDenotation import dotty.tools.dotc.core.Flags.* -import dotty.tools.dotc.core.NameOps.moduleClassName +import dotty.tools.dotc.core.NameOps.* import dotty.tools.dotc.core.Names.* import dotty.tools.dotc.core.Scopes.EmptyScope import dotty.tools.dotc.core.Symbols.* import dotty.tools.dotc.core.Types.* +import dotty.tools.dotc.interactive.Completion import dotty.tools.dotc.interactive.Interactive import dotty.tools.dotc.typer.ImportInfo +import dotty.tools.dotc.util.SourcePosition import dotty.tools.pc.IndexedContext.Result import dotty.tools.pc.utils.InteractiveEnrichments.* sealed trait IndexedContext: given ctx: Context def scopeSymbols: List[Symbol] - def names: IndexedContext.Names def rename(sym: Symbol): Option[String] - def outer: IndexedContext - - def findSymbol(name: String): Option[List[Symbol]] - - final def findSymbol(name: Name): Option[List[Symbol]] = - findSymbol(name.decoded) + def findSymbol(name: Name): Option[List[Symbol]] + def findSymbolInLocalScope(name: String): Option[List[Symbol]] final def lookupSym(sym: Symbol): Result = - findSymbol(sym.decodedName) match - case Some(symbols) if symbols.exists(_ == sym) => - Result.InScope - case Some(symbols) - if symbols.exists(s => isNotConflictingWithDefault(s, sym) || isTypeAliasOf(s, sym) || isTermAliasOf(s, sym)) => - Result.InScope - // when all the conflicting symbols came from an old version of the file + def all(symbol: Symbol): Set[Symbol] = Set(symbol, symbol.companionModule, symbol.companionClass, symbol.companion).filter(_ != NoSymbol) + val isRelated = all(sym) ++ all(sym.dealiasType) + findSymbol(sym.name) match + case Some(symbols) if symbols.exists(isRelated) => Result.InScope + case Some(symbols) if symbols.exists(isTermAliasOf(_, sym)) => Result.InScope + case Some(symbols) if symbols.map(_.dealiasType).exists(isRelated) => Result.InScope case Some(symbols) if symbols.nonEmpty && symbols.forall(_.isStale) => Result.Missing case Some(symbols) if symbols.exists(rename(_).isEmpty) => Result.Conflict + case Some(symbols) => Result.InScope case _ => Result.Missing end lookupSym - /** - * Scala by default imports following packages: - * https://scala-lang.org/files/archive/spec/3.4/02-identifiers-names-and-scopes.html - * import java.lang.* - * { - * import scala.* - * { - * import Predef.* - * { /* source */ } - * } - * } - * - * This check is necessary for proper scope resolution, because when we compare symbols from - * index including the underlying type like scala.collection.immutable.List it actually - * is in current scope in form of type forwarder imported from Predef. - */ - private def isNotConflictingWithDefault(sym: Symbol, queriedSym: Symbol): Boolean = - sym.info.widenDealias =:= queriedSym.info.widenDealias && (Interactive.isImportedByDefault(sym)) - final def hasRename(sym: Symbol, as: String): Boolean = rename(sym) match - case Some(v) => v == as + case Some(v) => + v == as case None => false // detects import scope aliases like @@ -74,73 +55,74 @@ sealed trait IndexedContext: case _ => false ) - private def isTypeAliasOf(alias: Symbol, queriedSym: Symbol): Boolean = - alias.isAliasType && alias.info.deepDealias.typeSymbol == queriedSym - - final def isEmpty: Boolean = this match - case IndexedContext.Empty => true - case _ => false - - final def importContext: IndexedContext = - this match - case IndexedContext.Empty => this - case _ if ctx.owner.is(Package) => this - case _ => outer.importContext - @tailrec - final def toplevelClashes(sym: Symbol): Boolean = + final def toplevelClashes(sym: Symbol, inImportScope: Boolean): Boolean = if sym == NoSymbol || sym.owner == NoSymbol || sym.owner.isRoot then - lookupSym(sym) match - case IndexedContext.Result.Conflict => true + val possibleConflictingSymbols = findSymbolInLocalScope(sym.name.show) + // if it's import scope we only care about toplevel conflicts, not any clashes inside objects etc. + val symbolClashes = if inImportScope then + // It's toplevel if it's parent is a package + possibleConflictingSymbols.filter(_.exists(_.owner.is(Package))) + else + possibleConflictingSymbols + symbolClashes match + case Some(symbols) if !symbols.contains(sym) => true case _ => false - else toplevelClashes(sym.owner) + else toplevelClashes(sym.owner, inImportScope) end IndexedContext object IndexedContext: - def apply(ctx: Context): IndexedContext = + def apply(pos: SourcePosition)(using Context): IndexedContext = ctx match case NoContext => Empty - case _ => LazyWrapper(using ctx) + case _ => LazyWrapper(pos)(using ctx) case object Empty extends IndexedContext: given ctx: Context = NoContext - def findSymbol(name: String): Option[List[Symbol]] = None + def findSymbol(name: Name): Option[List[Symbol]] = None + def findSymbolInLocalScope(name: String): Option[List[Symbol]] = None def scopeSymbols: List[Symbol] = List.empty - val names: Names = Names(Map.empty, Map.empty) def rename(sym: Symbol): Option[String] = None - def outer: IndexedContext = this - - class LazyWrapper(using val ctx: Context) extends IndexedContext: - val outer: IndexedContext = IndexedContext(ctx.outer) - val names: Names = extractNames(ctx) - def findSymbol(name: String): Option[List[Symbol]] = - names.symbols - .get(name) - .map(_.toList) - .orElse(outer.findSymbol(name)) + class LazyWrapper(pos: SourcePosition)(using val ctx: Context) extends IndexedContext: + + val completionContext = Completion.scopeContext(pos) + val names: Map[String, Seq[SingleDenotation]] = completionContext.names.toList.groupBy(_._1.show).map{ + case (name, denotations) => + val denots = denotations.flatMap(_._2) + val nonRoot = denots.filter(!_.symbol.owner.isRoot) + val (importedByDefault, conflictingValue) = denots.partition(denot => Interactive.isImportedByDefault(denot.symbol)) + if importedByDefault.nonEmpty && conflictingValue.nonEmpty then + name.trim -> conflictingValue + else + name.trim -> nonRoot + } + val renames = completionContext.renames + + def defaultScopes(name: Name): Option[List[Symbol]] = + List(defn.ScalaPredefModuleClass, defn.ScalaPackageClass, defn.JavaLangPackageClass) + .map(_.membersNamed(name)) + .collect { case denot if denot.exists => denot.first.symbol } + .toList match + case Nil => None + case list => Some(list) + + override def findSymbolInLocalScope(name: String): Option[List[Symbol]] = + names.get(name).map(_.map(_.symbol).toList).filter(_.nonEmpty) + def findSymbol(name: Name): Option[List[Symbol]] = + names + .get(name.show) + .map(_.map(_.symbol).toList) + .orElse(defaultScopes(name)) def scopeSymbols: List[Symbol] = - val acc = Set.newBuilder[Symbol] - (this :: outers).foreach { ref => - acc ++= ref.names.symbols.values.flatten - } - acc.result.toList + names.values.flatten.map(_.symbol).toList def rename(sym: Symbol): Option[String] = - names.renames - .get(sym) - .orElse(outer.rename(sym)) - - private def outers: List[IndexedContext] = - val builder = List.newBuilder[IndexedContext] - var curr = outer - while !curr.isEmpty do - builder += curr - curr = curr.outer - builder.result + renames.get(sym).orElse(renames.get(sym.companion)).map(_.decoded) + end LazyWrapper enum Result: @@ -149,97 +131,5 @@ object IndexedContext: case InScope | Conflict => true case Missing => false - case class Names( - symbols: Map[String, List[Symbol]], - renames: Map[Symbol, String] - ) - - private def extractNames(ctx: Context): Names = - def isAccessibleFromSafe(sym: Symbol, site: Type): Boolean = - try sym.isAccessibleFrom(site, superAccess = false) - catch - case NonFatal(e) => - false - - def accessibleSymbols(site: Type, tpe: Type)(using - Context - ): List[Symbol] = - tpe.decls.toList.filter(sym => isAccessibleFromSafe(sym, site)) - - def accesibleMembers(site: Type)(using Context): List[Symbol] = - site.allMembers - .filter(denot => - try isAccessibleFromSafe(denot.symbol, site) - catch - case NonFatal(e) => - false - ) - .map(_.symbol) - .toList - - def allAccessibleSymbols( - tpe: Type, - filter: Symbol => Boolean = _ => true - )(using Context): List[Symbol] = - val initial = accessibleSymbols(tpe, tpe).filter(filter) - val fromPackageObjects = - initial - .filter(_.isPackageObject) - .flatMap(sym => accessibleSymbols(tpe, sym.thisType)) - initial ++ fromPackageObjects - - def fromImport(site: Type, name: Name)(using Context): List[Symbol] = - List( - site.member(name.toTypeName), - site.member(name.toTermName), - site.member(name.moduleClassName), - ) - .flatMap(_.alternatives) - .map(_.symbol) - - def fromImportInfo( - imp: ImportInfo - )(using Context): List[(Symbol, Option[TermName])] = - val excludedNames = imp.excluded.map(_.decoded) - - if imp.isWildcardImport then - allAccessibleSymbols( - imp.site, - sym => !excludedNames.contains(sym.name.decoded) - ).map((_, None)) - else - imp.forwardMapping.toList.flatMap { (name, rename) => - val isRename = name != rename - if !isRename && !excludedNames.contains(name.decoded) then - fromImport(imp.site, name).map((_, None)) - else if isRename then - fromImport(imp.site, name).map((_, Some(rename))) - else Nil - } - end if - end fromImportInfo - - given Context = ctx - val (symbols, renames) = - if ctx.isImportContext then - val (syms, renames) = - fromImportInfo(ctx.importInfo.nn) - .map((sym, rename) => (sym, rename.map(r => sym -> r.decoded))) - .unzip - (syms, renames.flatten.toMap) - else if ctx.owner.isClass then - val site = ctx.owner.thisType - (accesibleMembers(site), Map.empty) - else if ctx.scope != EmptyScope then (ctx.scope.toList, Map.empty) - else (List.empty, Map.empty) - - val initial = Map.empty[String, List[Symbol]] - val values = - symbols.foldLeft(initial) { (acc, sym) => - val name = sym.decodedName - val syms = acc.getOrElse(name, List.empty) - acc.updated(name, sym :: syms) - } - Names(values, renames) - end extractNames + end IndexedContext diff --git a/presentation-compiler/src/main/dotty/tools/pc/InferExpectedType.scala b/presentation-compiler/src/main/dotty/tools/pc/InferExpectedType.scala index 3d65f69621e1..075167f3f5c1 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/InferExpectedType.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/InferExpectedType.scala @@ -51,7 +51,7 @@ class InferExpectedType( ) val locatedCtx = Interactive.contextOfPath(tpdPath)(using newctx) - val indexedCtx = IndexedContext(locatedCtx) + val indexedCtx = IndexedContext(pos)(using locatedCtx) val printer = ShortenedTypePrinter(search, IncludeDefaultParam.ResolveLater)(using indexedCtx) InterCompletionType.inferType(path)(using newctx).map{ diff --git a/presentation-compiler/src/main/dotty/tools/pc/InferredTypeProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/InferredTypeProvider.scala index a0d726d5f382..2006774ae19b 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/InferredTypeProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/InferredTypeProvider.scala @@ -75,7 +75,7 @@ final class InferredTypeProvider( Interactive.pathTo(driver.openedTrees(uri), pos)(using driver.currentCtx) given locatedCtx: Context = driver.localContext(params) - val indexedCtx = IndexedContext(locatedCtx) + val indexedCtx = IndexedContext(pos)(using locatedCtx) val autoImportsGen = AutoImports.generator( pos, sourceText, diff --git a/presentation-compiler/src/main/dotty/tools/pc/MetalsInteractive.scala b/presentation-compiler/src/main/dotty/tools/pc/MetalsInteractive.scala index ef583ea2a225..8132b0cf95cb 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/MetalsInteractive.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/MetalsInteractive.scala @@ -100,9 +100,9 @@ object MetalsInteractive: pos: SourcePosition, indexed: IndexedContext, skipCheckOnName: Boolean = false - ): List[Symbol] = + )(using Context): List[Symbol] = enclosingSymbolsWithExpressionType(path, pos, indexed, skipCheckOnName) - .map(_._1) + .map(_._1.sourceSymbol) /** * Returns the list of tuple enclosing symbol and @@ -217,17 +217,19 @@ object MetalsInteractive: val tpe = getIndex(t2).getOrElse(NoType) List((ddef.symbol, tpe, Some(name))) + case head :: (sel @ Select(_, name)) :: _ + if head.sourcePos.encloses(sel.sourcePos) && (name == StdNames.nme.apply || name == StdNames.nme.unapply) => + val optObjectSymbol = List(head.symbol).filter(sym => !(sym.is(Synthetic) && sym.is(Module))) + val classSymbol = head.symbol.companionClass + val optApplySymbol = List(sel.symbol).filter(sym => !sym.is(Synthetic)) + val symbols = optObjectSymbol ++ (classSymbol :: optApplySymbol) + symbols.collect: + case sym if sym.exists => (sym, sym.info, None) + case path @ head :: tail => if head.symbol.is(Exported) then val sym = head.symbol.sourceSymbol List((sym, sym.info, None)) - else if head.symbol.is(Synthetic) then - enclosingSymbolsWithExpressionType( - tail, - pos, - indexed, - skipCheckOnName - ) else if head.symbol != NoSymbol then if skipCheckOnName || MetalsInteractive.isOnName( @@ -236,6 +238,13 @@ object MetalsInteractive: indexed.ctx.source ) then List((head.symbol, head.typeOpt, None)) + else if head.symbol.is(Synthetic) then + enclosingSymbolsWithExpressionType( + tail, + pos, + indexed, + skipCheckOnName + ) /* Type tree for List(1) has an Int type variable, which has span * but doesn't exist in code. * https://github.com/scala/scala3/issues/15937 diff --git a/presentation-compiler/src/main/dotty/tools/pc/PcCollector.scala b/presentation-compiler/src/main/dotty/tools/pc/PcCollector.scala index 1ebfd405768e..d145272ba01d 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/PcCollector.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/PcCollector.scala @@ -160,7 +160,7 @@ trait PcCollector[T]: def collectEndMarker = EndMarker.getPosition(df, pos, sourceText).map: collect(EndMarker(df.symbol), _) - val annots = collectTrees(df.mods.annotations) + val annots = collectTrees(df.symbol.annotations.map(_.tree)) val traverser = new PcCollector.DeepFolderWithParent[Set[T]]( collectNamesWithParent @@ -215,8 +215,8 @@ trait PcCollector[T]: * @<>("") * def params() = ??? */ - case mdf: MemberDef if mdf.mods.annotations.nonEmpty => - val trees = collectTrees(mdf.mods.annotations) + case mdf: MemberDef if mdf.symbol.annotations.nonEmpty => + val trees = collectTrees(mdf.symbol.annotations.map(_.tree)) val traverser = new PcCollector.DeepFolderWithParent[Set[T]]( collectNamesWithParent @@ -315,7 +315,7 @@ object EndMarker: def getPosition(df: NamedDefTree, pos: SourcePosition, sourceText: String)( implicit ct: Context ): Option[SourcePosition] = - val name = df.name.toString() + val name = df.name.toString().stripSuffix("$") val endMarkerLine = sourceText.slice(df.span.start, df.span.end).split('\n').last val index = endMarkerLine.length() - name.length() diff --git a/presentation-compiler/src/main/dotty/tools/pc/PcDefinitionProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/PcDefinitionProvider.scala index 8ff43ba07358..ca5a36cefad0 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/PcDefinitionProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/PcDefinitionProvider.scala @@ -51,7 +51,7 @@ class PcDefinitionProvider( Interactive.pathTo(driver.openedTrees(uri), pos)(using driver.currentCtx) given ctx: Context = driver.localContext(params) - val indexedContext = IndexedContext(ctx) + val indexedContext = IndexedContext(pos)(using ctx) val result = if findTypeDef then findTypeDefinitions(path, pos, indexedContext, uri) else findDefinitions(path, pos, indexedContext, uri) @@ -79,7 +79,7 @@ class PcDefinitionProvider( .untypedPath(pos.span) .collect { case t: untpd.Tree => t } - definitionsForSymbol(untpdPath.headOption.map(_.symbol).toList, uri, pos) + definitionsForSymbols(untpdPath.headOption.map(_.symbol).toList, uri, pos) end fallbackToUntyped private def findDefinitions( @@ -89,7 +89,7 @@ class PcDefinitionProvider( uri: URI, ): DefinitionResult = import indexed.ctx - definitionsForSymbol( + definitionsForSymbols( MetalsInteractive.enclosingSymbols(path, pos, indexed), uri, pos @@ -113,68 +113,58 @@ class PcDefinitionProvider( case Nil => path.headOption match case Some(value: Literal) => - definitionsForSymbol(List(value.typeOpt.widen.typeSymbol), uri, pos) + definitionsForSymbols(List(value.typeOpt.widen.typeSymbol), uri, pos) case _ => DefinitionResultImpl.empty case _ => - definitionsForSymbol(typeSymbols, uri, pos) - + definitionsForSymbols(typeSymbols, uri, pos) end findTypeDefinitions - private def definitionsForSymbol( + private def definitionsForSymbols( symbols: List[Symbol], uri: URI, pos: SourcePosition )(using ctx: Context): DefinitionResult = - symbols match - case symbols @ (sym :: other) => - val isLocal = sym.source == pos.source - if isLocal then - val include = Include.definitions | Include.local - val (exportedDefs, otherDefs) = - Interactive.findTreesMatching(driver.openedTrees(uri), include, sym) - .partition(_.tree.symbol.is(Exported)) - - otherDefs.headOption.orElse(exportedDefs.headOption) match - case Some(srcTree) => - val pos = srcTree.namePos - if pos.exists then - val loc = new Location(params.uri().toString(), pos.toLsp) - DefinitionResultImpl( - SemanticdbSymbols.symbolName(sym), - List(loc).asJava, - ) - else DefinitionResultImpl.empty - case None => - DefinitionResultImpl.empty - else - val res = new ArrayList[Location]() - semanticSymbolsSorted(symbols) - .foreach { sym => - res.addAll(search.definition(sym, params.uri())) - } - DefinitionResultImpl( - SemanticdbSymbols.symbolName(sym), - res - ) - end if + semanticSymbolsSorted(symbols) match case Nil => DefinitionResultImpl.empty - end match - end definitionsForSymbol + case syms @ ((_, headSym) :: tail) => + val locations = syms.flatMap: + case (sym, semanticdbSymbol) => + locationsForSymbol(sym, semanticdbSymbol, uri, pos) + DefinitionResultImpl(headSym, locations.asJava) + + private def locationsForSymbol( + symbol: Symbol, + semanticdbSymbol: String, + uri: URI, + pos: SourcePosition + )(using ctx: Context): List[Location] = + val isLocal = symbol.source == pos.source + if isLocal then + val trees = driver.openedTrees(uri) + val include = Include.definitions | Include.local + val (exportedDefs, otherDefs) = + Interactive.findTreesMatching(trees, include, symbol) + .partition(_.tree.symbol.is(Exported)) + otherDefs.headOption.orElse(exportedDefs.headOption).collect: + case srcTree if srcTree.namePos.exists => + new Location(params.uri().toString(), srcTree.namePos.toLsp) + .toList + else search.definition(semanticdbSymbol, uri).asScala.toList def semanticSymbolsSorted( syms: List[Symbol] - )(using ctx: Context): List[String] = + )(using ctx: Context): List[(Symbol, String)] = syms - .map { sym => + .collect { case sym if sym.exists => // in case of having the same type and teerm symbol // term comes first // used only for ordering symbols that come from `Import` val termFlag = if sym.is(ModuleClass) then sym.sourceModule.isTerm else sym.isTerm - (termFlag, SemanticdbSymbols.symbolName(sym)) + (termFlag, sym.sourceSymbol, SemanticdbSymbols.symbolName(sym)) } - .sorted - .map(_._2) + .sortBy { case (termFlag, _, name) => (termFlag, name) } + .map(_.tail) end PcDefinitionProvider diff --git a/presentation-compiler/src/main/dotty/tools/pc/PcInlayHintsProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/PcInlayHintsProvider.scala index cf4929dfc91d..8718eaf58a88 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/PcInlayHintsProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/PcInlayHintsProvider.scala @@ -125,7 +125,7 @@ class PcInlayHintsProvider( val tpdPath = Interactive.pathTo(unit.tpdTree, pos.span) - val indexedCtx = IndexedContext(Interactive.contextOfPath(tpdPath)) + val indexedCtx = IndexedContext(pos)(using Interactive.contextOfPath(tpdPath)) val printer = ShortenedTypePrinter( symbolSearch )(using indexedCtx) @@ -149,7 +149,7 @@ class PcInlayHintsProvider( InlayHints.makeLabelParts(parts, tpeStr) end toLabelParts - private val definitions = IndexedContext(ctx).ctx.definitions + private val definitions = IndexedContext(pos)(using ctx).ctx.definitions private def syntheticTupleApply(tree: Tree): Boolean = tree match case sel: Select => diff --git a/presentation-compiler/src/main/dotty/tools/pc/PcInlineValueProviderImpl.scala b/presentation-compiler/src/main/dotty/tools/pc/PcInlineValueProvider.scala similarity index 58% rename from presentation-compiler/src/main/dotty/tools/pc/PcInlineValueProviderImpl.scala rename to presentation-compiler/src/main/dotty/tools/pc/PcInlineValueProvider.scala index fc4b53e60bbd..c35046db2fc4 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/PcInlineValueProviderImpl.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/PcInlineValueProvider.scala @@ -16,17 +16,76 @@ import dotty.tools.dotc.core.StdNames import dotty.tools.dotc.core.Symbols.Symbol import dotty.tools.dotc.interactive.Interactive import dotty.tools.dotc.interactive.InteractiveDriver +import dotty.tools.dotc.util.SourceFile import dotty.tools.dotc.util.SourcePosition import dotty.tools.pc.utils.InteractiveEnrichments.* import dotty.tools.pc.IndexedContext.Result import org.eclipse.lsp4j as l -final class PcInlineValueProviderImpl( +final class PcInlineValueProvider( driver: InteractiveDriver, val params: OffsetParams -) extends WithSymbolSearchCollector[Option[Occurence]](driver, params) - with InlineValueProvider: +) extends WithSymbolSearchCollector[Option[Occurence]](driver, params): + + // We return a result or an error + def getInlineTextEdits(): Either[String, List[l.TextEdit]] = + defAndRefs() match { + case Right((defn, refs)) => + val edits = + if (defn.shouldBeRemoved) { + val defEdit = definitionTextEdit(defn) + val refsEdits = refs.map(referenceTextEdit(defn)) + defEdit :: refsEdits + } else refs.map(referenceTextEdit(defn)) + Right(edits) + case Left(error) => Left(error) + } + + private def referenceTextEdit( + definition: Definition + )(ref: Reference): l.TextEdit = + if (definition.requiresBrackets && ref.requiresBrackets) + new l.TextEdit( + ref.range, + s"""(${ref.rhs})""" + ) + else new l.TextEdit(ref.range, ref.rhs) + + private def definitionTextEdit(definition: Definition): l.TextEdit = + new l.TextEdit( + extend( + definition.rangeOffsets.start, + definition.rangeOffsets.end, + definition.range + ), + "" + ) + + private def extend( + startOffset: Int, + endOffset: Int, + range: l.Range + ): l.Range = { + val (startWithSpace, endWithSpace): (Int, Int) = + extendRangeToIncludeWhiteCharsAndTheFollowingNewLine( + text + )(startOffset, endOffset) + val startPos = new l.Position( + range.getStart.getLine, + range.getStart.getCharacter - (startOffset - startWithSpace) + ) + val endPos = + if (endWithSpace - 1 >= 0 && text(endWithSpace - 1) == '\n') + new l.Position(range.getEnd.getLine + 1, 0) + else + new l.Position( + range.getEnd.getLine, + range.getEnd.getCharacter + endWithSpace - endOffset + ) + + new l.Range(startPos, endPos) + } val position: l.Position = pos.toLsp.getStart().nn @@ -41,7 +100,7 @@ final class PcInlineValueProviderImpl( Some(Occurence(tree, parent, adjustedPos)) case _ => None - override def defAndRefs(): Either[String, (Definition, List[Reference])] = + def defAndRefs(): Either[String, (Definition, List[Reference])] = val newctx = driver.currentCtx.fresh.setCompilationUnit(unit) val allOccurences = result().flatten for @@ -51,8 +110,8 @@ final class PcInlineValueProviderImpl( } .toRight(Errors.didNotFindDefinition) path = Interactive.pathTo(unit.tpdTree, definition.tree.rhs.span)(using newctx) - indexedContext = IndexedContext(Interactive.contextOfPath(path)(using newctx)) - symbols = symbolsUsedInDefn(definition.tree.rhs).filter(indexedContext.lookupSym(_) == Result.InScope) + indexedContext = IndexedContext(definition.tree.namePos)(using Interactive.contextOfPath(path)(using newctx)) + symbols = symbolsUsedInDefn(definition.tree.rhs, indexedContext) references <- getReferencesToInline(definition, allOccurences, symbols) yield val (deleteDefinition, refsEdits) = references @@ -60,7 +119,6 @@ final class PcInlineValueProviderImpl( val defPos = definition.tree.sourcePos val defEdit = Definition( defPos.toLsp, - adjustRhs(definition.tree.rhs.sourcePos), RangeOffset(defPos.start, defPos.end), definitionRequiresBrackets(definition.tree.rhs)(using newctx), deleteDefinition @@ -70,6 +128,15 @@ final class PcInlineValueProviderImpl( end for end defAndRefs + private def stripIndentPrefix(rhs: String, refIndent: String, defIndent: String, hasNextLineAfterEqualsSign: Boolean): String = + val rhsLines = rhs.split("\n").toList + rhsLines match + case h :: Nil => rhs + case h :: t => + val header = if !hasNextLineAfterEqualsSign then h else "\n" ++ refIndent ++ " " ++ h + header ++ t.map(refIndent ++ _.stripPrefix(defIndent)).mkString("\n", "\n", "") + case Nil => rhs + private def definitionRequiresBrackets(tree: Tree)(using Context): Boolean = NavigateAST .untypedPath(tree.span) @@ -102,27 +169,31 @@ final class PcInlineValueProviderImpl( end referenceRequiresBrackets - private def adjustRhs(pos: SourcePosition) = + private def extendWithSurroundingParens(pos: SourcePosition) = + /** Move `point` by `step` as long as the character at `point` is `acceptedChar` */ def extend(point: Int, acceptedChar: Char, step: Int): Int = val newPoint = point + step - if newPoint > 0 && newPoint < text.length && text( - newPoint - ) == acceptedChar + if newPoint > 0 && newPoint < text.length && + text(newPoint) == acceptedChar then extend(newPoint, acceptedChar, step) else point val adjustedStart = extend(pos.start, '(', -1) val adjustedEnd = extend(pos.end - 1, ')', 1) + 1 text.slice(adjustedStart, adjustedEnd).mkString - private def symbolsUsedInDefn(rhs: Tree): Set[Symbol] = + private def symbolsUsedInDefn(rhs: Tree, indexedContext: IndexedContext): Set[Symbol] = def collectNames( symbols: Set[Symbol], tree: Tree ): Set[Symbol] = tree match - case id: (Ident | Select) + case id: Ident if !id.symbol.is(Synthetic) && !id.symbol.is(Implicit) => symbols + tree.symbol + case sel: Select => + indexedContext.lookupSym(sel.symbol) match + case IndexedContext.Result.InScope => symbols + sel.symbol + case _ => symbols case _ => symbols val traverser = new DeepFolder[Set[Symbol]](collectNames) @@ -139,7 +210,7 @@ final class PcInlineValueProviderImpl( .exists(e => e.isTerm) def allreferences = allOccurences.filterNot(_.isDefn) def inlineAll() = - makeRefsEdits(allreferences, symbols).map((true, _)) + makeRefsEdits(allreferences, symbols, definition).map((true, _)) if definition.tree.sourcePos.toLsp.encloses(position) then if defIsLocal then inlineAll() else Left(Errors.notLocal) else @@ -150,21 +221,35 @@ final class PcInlineValueProviderImpl( ref <- list .find(_.pos.toLsp.encloses(position)) .toRight(Errors.didNotFindReference) - refEdits <- makeRefsEdits(List(ref), symbols) + refEdits <- makeRefsEdits(List(ref), symbols, definition) yield (false, refEdits) end if end getReferencesToInline + extension (pos: SourcePosition) + def startColumnIndentPadding: String = { + val source = pos.source + val offset = pos.start + var idx = source.startOfLine(offset) + val pad = new StringBuilder + while (idx != offset && idx < source.content().length && source.content()(idx).isWhitespace) { + pad.append(source.content()(idx)) + idx += 1 + } + pad.result() + } + private def makeRefsEdits( refs: List[Occurence], - symbols: Set[Symbol] + symbols: Set[Symbol], + definition: DefinitionTree ): Either[String, List[Reference]] = val newctx = driver.currentCtx.fresh.setCompilationUnit(unit) def buildRef(occurrence: Occurence): Either[String, Reference] = val path = Interactive.pathTo(unit.tpdTree, occurrence.pos.span)(using newctx) - val indexedContext = IndexedContext( - Interactive.contextOfPath(path)(using newctx) + val indexedContext = IndexedContext(pos)( + using Interactive.contextOfPath(path)(using newctx) ) import indexedContext.ctx val conflictingSymbols = symbols @@ -174,10 +259,18 @@ final class PcInlineValueProviderImpl( case _ => false } .map(_.fullNameBackticked) + val hasNextLineAfterEqualsSign = + definition.tree.sourcePos.startLine != definition.tree.rhs.sourcePos.startLine if conflictingSymbols.isEmpty then Right( Reference( occurrence.pos.toLsp, + stripIndentPrefix( + extendWithSurroundingParens(definition.tree.rhs.sourcePos), + occurrence.tree.startPos.startColumnIndentPadding, + definition.tree.startPos.startColumnIndentPadding, + hasNextLineAfterEqualsSign + ), occurrence.parent.map(p => RangeOffset(p.sourcePos.start, p.sourcePos.end) ), @@ -196,7 +289,7 @@ final class PcInlineValueProviderImpl( ) end makeRefsEdits -end PcInlineValueProviderImpl +end PcInlineValueProvider case class Occurence(tree: Tree, parent: Option[Tree], pos: SourcePosition): def isDefn = @@ -205,3 +298,19 @@ case class Occurence(tree: Tree, parent: Option[Tree], pos: SourcePosition): case _ => false case class DefinitionTree(tree: ValDef, pos: SourcePosition) + +case class RangeOffset(start: Int, end: Int) + +case class Definition( + range: l.Range, + rangeOffsets: RangeOffset, + requiresBrackets: Boolean, + shouldBeRemoved: Boolean +) + +case class Reference( + range: l.Range, + rhs: String, + parentOffsets: Option[RangeOffset], + requiresBrackets: Boolean +) diff --git a/presentation-compiler/src/main/dotty/tools/pc/ScalaPresentationCompiler.scala b/presentation-compiler/src/main/dotty/tools/pc/ScalaPresentationCompiler.scala index dc53525480c3..0359e21dae89 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/ScalaPresentationCompiler.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/ScalaPresentationCompiler.scala @@ -355,7 +355,7 @@ case class ScalaPresentationCompiler( val empty: Either[String, List[l.TextEdit]] = Right(List()) (compilerAccess .withInterruptableCompiler(empty, params.token()) { pc => - new PcInlineValueProviderImpl(pc.compiler(), params) + new PcInlineValueProvider(pc.compiler(), params) .getInlineTextEdits() }(params.toQueryContext)) .thenApply { diff --git a/presentation-compiler/src/main/dotty/tools/pc/SignatureHelpProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/SignatureHelpProvider.scala index bd16d2ce2aa9..5f925ea80ee7 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/SignatureHelpProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/SignatureHelpProvider.scala @@ -37,7 +37,7 @@ object SignatureHelpProvider: val path = Interactive.pathTo(unit.tpdTree, pos.span)(using driver.currentCtx) val localizedContext = Interactive.contextOfPath(path)(using driver.currentCtx) - val indexedContext = IndexedContext(driver.currentCtx) + val indexedContext = IndexedContext(pos)(using driver.currentCtx) given Context = localizedContext.fresh .setCompilationUnit(unit) diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala index 2a63d6a92a81..ef9f77eb58fc 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala @@ -107,7 +107,7 @@ class CompletionProvider( val locatedCtx = Interactive.contextOfPath(tpdPath)(using newctx) - val indexedCtx = IndexedContext(locatedCtx) + val indexedCtx = IndexedContext(pos)(using locatedCtx) val completionPos = CompletionPos.infer(pos, params, adjustedPath, wasCursorApplied)(using locatedCtx) @@ -222,7 +222,6 @@ class CompletionProvider( if config.isDetailIncludedInLabel then completion.labelWithDescription(printer) else completion.label val ident = underlyingCompletion.insertText.getOrElse(underlyingCompletion.label) - lazy val isInStringInterpolation = path match // s"My name is $name" diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionValue.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionValue.scala index 90b285bffb3a..05d97972d76e 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionValue.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionValue.scala @@ -261,13 +261,13 @@ object CompletionValue: end NamedArg case class Autofill( - value: String + value: String, + override val label: String, ) extends CompletionValue: override def completionItemKind(using Context): CompletionItemKind = CompletionItemKind.Enum override def completionItemDataKind: Integer = CompletionSource.OverrideKind.ordinal override def insertText: Option[String] = Some(value) - override def label: String = "Autofill with default values" case class Keyword(label: String, override val insertText: Option[String]) extends CompletionValue: diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala index 05dbe1ef5a43..4b2e76807895 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala @@ -569,6 +569,7 @@ class Completions( then indexedContext.lookupSym(sym) match case IndexedContext.Result.InScope => false + case IndexedContext.Result.Missing if indexedContext.rename(sym).isDefined => false case _ if completionMode.is(Mode.ImportOrExport) => visit( CompletionValue.Workspace( diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/MatchCaseCompletions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/MatchCaseCompletions.scala index 2efcba48e82d..4fbf22e2294c 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/MatchCaseCompletions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/MatchCaseCompletions.scala @@ -147,7 +147,7 @@ object CaseKeywordCompletion: definitions.NullClass, definitions.NothingClass, ) - val tpes = Set(selectorSym, selectorSym.companion) + val tpes = Set(selectorSym, selectorSym.companion).filter(_ != NoSymbol) def isSubclass(sym: Symbol) = tpes.exists(par => sym.isSubClass(par)) def visit(symImport: SymbolImport): Unit = @@ -174,8 +174,9 @@ object CaseKeywordCompletion: indexedContext.scopeSymbols .foreach(s => - val ts = s.info.deepDealias.typeSymbol - if isValid(ts) then visit(autoImportsGen.inferSymbolImport(ts)) + val ts = if s.is(Flags.Module) then s.info.typeSymbol else s.dealiasType + if isValid(ts) then + visit(autoImportsGen.inferSymbolImport(ts)) ) // Step 2: walk through known subclasses of sealed types. val sealedDescs = subclassesForType( @@ -185,6 +186,7 @@ object CaseKeywordCompletion: val symbolImport = autoImportsGen.inferSymbolImport(sym) visit(symbolImport) } + val res = result.result().flatMap { case si @ SymbolImport(sym, name, importSel) => completionGenerator.labelForCaseMember(sym, name.value).map { @@ -293,7 +295,6 @@ object CaseKeywordCompletion: val (labels, imports) = sortedSubclasses.map((si, label) => (label, si.importSel)).unzip - val (obracket, cbracket) = if noIndent then (" {", "}") else ("", "") val basicMatch = CompletionValue.MatchCompletion( "match", diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/NamedArgCompletions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/NamedArgCompletions.scala index dd3a910beb4f..7b88d8edfbc8 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/NamedArgCompletions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/NamedArgCompletions.scala @@ -30,6 +30,9 @@ import dotty.tools.dotc.core.Types.WildcardType import dotty.tools.pc.IndexedContext import dotty.tools.pc.utils.InteractiveEnrichments.* import scala.annotation.tailrec +import dotty.tools.dotc.core.Denotations.Denotation +import dotty.tools.dotc.core.Denotations.MultiDenotation +import dotty.tools.dotc.core.Denotations.SingleDenotation object NamedArgCompletions: @@ -136,46 +139,42 @@ object NamedArgCompletions: // fallback for when multiple overloaded methods match the supplied args def fallbackFindMatchingMethods() = - def maybeNameAndIndexedContext( + def matchingMethodsSymbols( method: Tree - ): Option[(Name, IndexedContext)] = + ): List[Symbol] = method match - case Ident(name) => Some((name, indexedContext)) - case Select(This(_), name) => Some((name, indexedContext)) - case Select(from, name) => + case Ident(name) => indexedContext.findSymbol(name).getOrElse(Nil) + case Select(This(_), name) => indexedContext.findSymbol(name).getOrElse(Nil) + case sel @ Select(from, name) => val symbol = from.symbol val ownerSymbol = if symbol.is(Method) && symbol.owner.isClass then Some(symbol.owner) else Try(symbol.info.classSymbol).toOption - ownerSymbol.map(sym => - (name, IndexedContext(context.localContext(from, sym))) - ) - case Apply(fun, _) => maybeNameAndIndexedContext(fun) - case _ => None + ownerSymbol.map(sym => sym.info.member(name)).collect{ + case single: SingleDenotation => List(single.symbol) + case multi: MultiDenotation => multi.allSymbols + }.getOrElse(Nil) + case Apply(fun, _) => matchingMethodsSymbols(fun) + case _ => Nil val matchingMethods = for - (name, indexedContext) <- maybeNameAndIndexedContext(method) - potentialMatches <- indexedContext.findSymbol(name) - yield - potentialMatches.collect { - case m - if m.is(Flags.Method) && - m.vparamss.length >= argss.length && - Try(m.isAccessibleFrom(apply.symbol.info)).toOption + potentialMatch <- matchingMethodsSymbols(method) + if potentialMatch.is(Flags.Method) && + potentialMatch.vparamss.length >= argss.length && + Try(potentialMatch.isAccessibleFrom(apply.symbol.info)).toOption .getOrElse(false) && - m.vparamss + potentialMatch.vparamss .zip(argss) .reverse .zipWithIndex .forall { case (pair, index) => - FuzzyArgMatcher(m.tparams) + FuzzyArgMatcher(potentialMatch.tparams) .doMatch(allArgsProvided = index != 0, ident) .tupled(pair) - } => - m - } - matchingMethods.getOrElse(Nil) + } + yield potentialMatch + matchingMethods end fallbackFindMatchingMethods val matchingMethods: List[Symbols.Symbol] = @@ -339,9 +338,16 @@ object NamedArgCompletions: s"${param.nameBackticked.replace("$", "$$")} = $${${index + 1}${findDefaultValue(param)}}" } .mkString(", ") + val labelText = allParams + .collect { + case param if !param.symbol.is(Flags.HasDefault) => + s"${param.nameBackticked.replace("$", "$$")} = ???" + } + .mkString(", ") List( CompletionValue.Autofill( - editText + editText, + labelText, ) ) else List.empty diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/OverrideCompletions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/OverrideCompletions.scala index f5c15ca6df0e..8123bc8fa216 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/OverrideCompletions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/OverrideCompletions.scala @@ -191,7 +191,7 @@ object OverrideCompletions: template :: path case path => path - val indexedContext = IndexedContext( + val indexedContext = IndexedContext(pos)(using Interactive.contextOfPath(path)(using newctx) ) import indexedContext.ctx diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/ScalaCliCompletions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/ScalaCliCompletions.scala index e2a0a033ee6b..8df727b14155 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/ScalaCliCompletions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/ScalaCliCompletions.scala @@ -13,14 +13,20 @@ class ScalaCliCompletions( ): def unapply(path: List[Tree]) = def scalaCliDep = CoursierComplete.isScalaCliDep( - pos.lineContent.take(pos.column).stripPrefix("/*