From 4c3b64f2d9cbfbde5cb6e4604448c52924d4d82a Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 18 Jul 2025 10:23:31 +0200 Subject: [PATCH 1/2] Make separation checking controlled by language import It's import language.experimental.separationChecking --- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 10 +++++++--- compiler/src/dotty/tools/dotc/config/Feature.scala | 2 ++ compiler/test/dotty/tools/dotc/CompilationTests.scala | 6 +++--- library/src/scala/runtime/stdLibPatches/language.scala | 7 +++++++ project/Build.scala | 2 +- .../captures => neg}/capture-vars-subtyping.scala | 2 +- .../captures => neg}/cc-fresh-levels.scala | 2 +- 7 files changed, 22 insertions(+), 9 deletions(-) rename tests/{neg-custom-args/captures => neg}/capture-vars-subtyping.scala (94%) rename tests/{neg-custom-args/captures => neg}/cc-fresh-levels.scala (94%) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index e8cc0eb69528..ab40a3faaa36 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -259,7 +259,10 @@ class CheckCaptures extends Recheck, SymTransformer: /** The set of symbols that were rechecked via a completer */ private val completed = new mutable.HashSet[Symbol] - var needAnotherRun = false + /** Set on recheckClassDef since there we see all language imports */ + private var sepChecksEnabled = false + + private var needAnotherRun = false def resetIteration()(using Context): Unit = needAnotherRun = false @@ -511,7 +514,7 @@ 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}") - if ccConfig.useSepChecks then + if sepChecksEnabled then recur(underlying.filter(!_.isTerminalCapability), 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 @@ -1144,6 +1147,7 @@ class CheckCaptures extends Recheck, SymTransformer: * is already done in the TypeApply. */ override def recheckClassDef(tree: TypeDef, impl: Template, cls: ClassSymbol)(using Context): Type = + if Feature.enabled(Feature.separationChecking) then sepChecksEnabled = true val localSet = capturedVars(cls) for parent <- impl.parents do // (1) checkSubset(capturedVars(parent.tpe.classSymbol), localSet, parent.srcPos, @@ -2014,7 +2018,7 @@ class CheckCaptures extends Recheck, SymTransformer: end checker checker.traverse(unit)(using ctx.withOwner(defn.RootClass)) - if ccConfig.useSepChecks then SepCheck(this).traverse(unit) + if sepChecksEnabled 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 diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 3b96eb83731d..067c6b757ec7 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -32,6 +32,7 @@ object Feature: val saferExceptions = experimental("saferExceptions") val pureFunctions = experimental("pureFunctions") val captureChecking = experimental("captureChecking") + val separationChecking = experimental("separationChecking") val into = experimental("into") val modularity = experimental("modularity") val quotedPatternsWithPolymorphicFunctions = experimental("quotedPatternsWithPolymorphicFunctions") @@ -60,6 +61,7 @@ object Feature: (saferExceptions, "Enable safer exceptions"), (pureFunctions, "Enable pure functions for capture checking"), (captureChecking, "Enable experimental capture checking"), + (separationChecking, "Enable experimental separation checking (requires captureChecking)"), (into, "Allow into modifier on parameter types"), (modularity, "Enable experimental modularity features"), (packageObjectValues, "Enable experimental package objects as values"), diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 8ccfa43b1d41..c49efceff73f 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -37,7 +37,7 @@ class CompilationTests { compileFilesInDir("tests/pos-special/sourcepath/outer", defaultOptions.and("-sourcepath", "tests/pos-special/sourcepath")), compileFile("tests/pos-special/sourcepath/outer/nested/Test4.scala", defaultOptions.and("-sourcepath", "tests/pos-special/sourcepath")), compileFilesInDir("tests/pos-scala2", defaultOptions.and("-source", "3.0-migration")), - compileFilesInDir("tests/pos-custom-args/captures", defaultOptions.and("-language:experimental.captureChecking", "-source", "3.8")), + compileFilesInDir("tests/pos-custom-args/captures", defaultOptions.and("-language:experimental.captureChecking", "-language:experimental.separationChecking")), compileFile("tests/pos-special/utf8encoded.scala", defaultOptions.and("-encoding", "UTF8")), compileFile("tests/pos-special/utf16encoded.scala", defaultOptions.and("-encoding", "UTF16")), compileDir("tests/pos-special/i18589", defaultOptions.and("-Wsafe-init").without("-Ycheck:all")), @@ -149,7 +149,7 @@ class CompilationTests { aggregateTests( compileFilesInDir("tests/neg", defaultOptions, FileFilter.exclude(TestSources.negScala2LibraryTastyExcludelisted)), compileFilesInDir("tests/neg-deep-subtype", allowDeepSubtypes), - compileFilesInDir("tests/neg-custom-args/captures", defaultOptions.and("-language:experimental.captureChecking", "-source", "3.8")), + compileFilesInDir("tests/neg-custom-args/captures", defaultOptions.and("-language:experimental.captureChecking", "-language:experimental.separationChecking")), compileFile("tests/neg-custom-args/sourcepath/outer/nested/Test1.scala", defaultOptions.and("-sourcepath", "tests/neg-custom-args/sourcepath")), compileDir("tests/neg-custom-args/sourcepath2/hi", defaultOptions.and("-sourcepath", "tests/neg-custom-args/sourcepath2", "-Xfatal-warnings")), compileList("duplicate source", List( @@ -172,7 +172,7 @@ class CompilationTests { aggregateTests( compileFilesInDir("tests/run", defaultOptions.and("-Wsafe-init")), compileFilesInDir("tests/run-deep-subtype", allowDeepSubtypes), - compileFilesInDir("tests/run-custom-args/captures", allowDeepSubtypes.and("-language:experimental.captureChecking", "-source", "3.8")), + compileFilesInDir("tests/run-custom-args/captures", allowDeepSubtypes.and("-language:experimental.captureChecking", "-language:experimental.separationChecking")), // Run tests for legacy lazy vals. compileFilesInDir("tests/run", defaultOptions.and("-Wsafe-init", "-Ylegacy-lazy-vals", "-Ycheck-constraint-deps"), FileFilter.include(TestSources.runLazyValsAllowlist)), ).checkRuns() diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index 0f5e904e29bb..31ef81b41fa1 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -84,6 +84,13 @@ object language: @compileTimeOnly("`captureChecking` can only be used at compile time in import statements") object captureChecking + /** Experimental support for separation checking; requires captureChecking also to be enabled. + * + * @see [[https://dotty.epfl.ch/docs/reference/experimental/cc]] + */ + @compileTimeOnly("`separationChecking` can only be used at compile time in import statements") + object separationChecking + /** Experimental support for automatic conversions of arguments, without requiring * a language import `import scala.language.implicitConversions`. * diff --git a/project/Build.scala b/project/Build.scala index 158f72f97991..a215acd98d62 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1512,7 +1512,7 @@ object Build { settings(scala2LibraryBootstrappedSettings). settings( moduleName := "scala2-library-cc", - scalacOptions ++= Seq("-source", "3.8"), // for separation checking + scalacOptions += "-language:experimental.separationChecking" // for separation checking ) lazy val scala2LibraryBootstrappedSettings = Seq( diff --git a/tests/neg-custom-args/captures/capture-vars-subtyping.scala b/tests/neg/capture-vars-subtyping.scala similarity index 94% rename from tests/neg-custom-args/captures/capture-vars-subtyping.scala rename to tests/neg/capture-vars-subtyping.scala index 68b26dcf564d..5552448194f1 100644 --- a/tests/neg-custom-args/captures/capture-vars-subtyping.scala +++ b/tests/neg/capture-vars-subtyping.scala @@ -1,5 +1,5 @@ import language.experimental.captureChecking -import language.`3.7` // no separation checking, TODO enable +// no separation checking, TODO enable and move to neg-customargs import caps.* def test[C^] = diff --git a/tests/neg-custom-args/captures/cc-fresh-levels.scala b/tests/neg/cc-fresh-levels.scala similarity index 94% rename from tests/neg-custom-args/captures/cc-fresh-levels.scala rename to tests/neg/cc-fresh-levels.scala index 4cbd079ad627..b3415dd9539c 100644 --- a/tests/neg-custom-args/captures/cc-fresh-levels.scala +++ b/tests/neg/cc-fresh-levels.scala @@ -1,5 +1,5 @@ -//> using options -source 3.7 import language.experimental.captureChecking +// no separation checking import caps.* class IO class Ref[X](init: X): From aa13ddf8187dc235a7732aa54fe796bb7126da30 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 18 Jul 2025 12:27:56 +0200 Subject: [PATCH 2/2] Experimental bookkeeping chores --- compiler/src/dotty/tools/dotc/config/Feature.scala | 2 +- compiler/src/dotty/tools/dotc/typer/Checking.scala | 1 + project/MiMaFilters.scala | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 067c6b757ec7..23305a6b0333 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -41,7 +41,7 @@ object Feature: def experimentalAutoEnableFeatures(using Context): List[TermName] = defn.languageExperimentalFeatures .map(sym => experimental(sym.name)) - .filterNot(_ == captureChecking) // TODO is this correct? + .filterNot(sym => sym == captureChecking || sym == separationChecking) // TODO is this correct? val values = List( (nme.help, "Display all available features"), diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 1e4acd7ed360..ceec2392af1a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -926,6 +926,7 @@ object Checking { val name = Feature.experimental(sel.name) name == Feature.scala2macros || name == Feature.captureChecking + || name == Feature.separationChecking trees.filter { case Import(qual, selectors) => languageImport(qual) match diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index c57136b262dd..cc8051a01d43 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -16,6 +16,8 @@ object MiMaFilters { ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language.2.13"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$2$u002E13$"), + ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.separationChecking"), + ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$separationChecking$"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.Conversion.underlying"), ProblemFilters.exclude[MissingClassProblem]("scala.Conversion$"),