diff --git a/compiler/src/dotty/tools/dotc/Run.scala b/compiler/src/dotty/tools/dotc/Run.scala index af21f7389c07..be713954fd4b 100644 --- a/compiler/src/dotty/tools/dotc/Run.scala +++ b/compiler/src/dotty/tools/dotc/Run.scala @@ -375,7 +375,7 @@ extends ImplicitRunInfo, ConstraintRunInfo, cc.CaptureRunInfo { val pluginPlan = ctx.base.addPluginPhases(ctx.base.phasePlan) val phases = ctx.base.fusePhases(pluginPlan, - ctx.settings.Yskip.value, ctx.settings.YstopBefore.value, stopAfter, ctx.settings.Ycheck.value) + ctx.settings.Yskip.value, ctx.settings.YstopBefore.value, ctx.settings.Ycheck.value) ctx.base.usePhases(phases, runCtx) if ctx.settings.YnoDoubleBindings.value then @@ -390,7 +390,16 @@ extends ImplicitRunInfo, ConstraintRunInfo, cc.CaptureRunInfo { if (ctx.isBestEffort && phases.exists(_.phaseName == "typer")) Some("typer") else None - for phase <- allPhases do + def matchesStopAfter(p: Phase): Boolean = p match + case mp: dotty.tools.dotc.transform.MegaPhase => + mp.miniPhases.exists(sub => stopAfter.contains(sub.phaseName)) + case _ => + stopAfter.contains(p.phaseName) + + var stopped = false + var i = 0 + while i < allPhases.length && !stopped do + val phase = allPhases(i) doEnterPhase(phase) val phaseWillRun = phase.isRunnable || forceReachPhaseMaybe.nonEmpty if phaseWillRun then @@ -423,7 +432,9 @@ extends ImplicitRunInfo, ConstraintRunInfo, cc.CaptureRunInfo { end if end if doAdvancePhase(phase, wasRan = phaseWillRun) - end for + if matchesStopAfter(phase) then stopped = true + i += 1 + end while profiler.finished() } diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 87f9defa34e6..d411fafa3b04 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1396,13 +1396,15 @@ class CheckCaptures extends Recheck, SymTransformer: * conforms to the expected type where all inferred capture sets are dropped. * This ensures that if files compile separately, they will also compile * in a joint compilation. - * 2. If a val has an inferred type with a terminal capability in its span capset, - * check that it this capability is subsumed by the capset that was inferred + * 2. If a val has an inferred type with a terminal capability in its capture set, + * check that this capability is subsumed by the capset that was inferred * for the class from its other fields via `captureSetImpliedByFields`. * That capset is defined to take into account all fields but is computed * only from fields with explicitly given types in order to avoid cycles. * See comment on Setup.fieldsWithExplicitTypes. So we have to make sure * that fields with inferred types would not change that capset. + * REPL wrapper objects are exempt since they are invisible to the user + * and should not impose explicit type requirements on REPL definitions. */ def checkInferredResult(tp: Type, tree: ValOrDefDef)(using Context): Type = { val sym = tree.symbol @@ -1451,6 +1453,7 @@ class CheckCaptures extends Recheck, SymTransformer: cls.isPackageObject && cls.enclosingPackageClass.isEmptyPackage if sym.owner.isClass && !isToplevelDefsInEmptyPackage(sym.owner) + && !sym.owner.name.isReplWrapperName // REPL wrappers are invisible to the user && contributesLocalCapToClass(sym) && !CaptureSet.isAssumedPure(sym) then diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index d814269cf1c9..d4f3ccaacb97 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -528,7 +528,7 @@ private sealed trait YSettings: val Yskip: Setting[List[String]] = PhasesSetting(ForkSetting, "Yskip", "Skip") val YbackendParallelism: Setting[Int] = IntChoiceSetting(ForkSetting, "Ybackend-parallelism", "maximum worker threads for backend", 1 to 16, 1) val YbackendWorkerQueue: Setting[Int] = IntChoiceSetting(ForkSetting, "Ybackend-worker-queue", "backend threads worker queue size", 0 to 1000, 0) - val YstopAfter: Setting[List[String]] = PhasesSetting(ForkSetting, "Ystop-after", "Stop after", aliases = List("-stop")) // backward compat + val YstopAfter: Setting[List[String]] = PhasesSetting(ForkSetting, "Ystop-after", "Stop after the phase group containing the named phase. Mini-phases fused into a MegaPhase share a group, so the rest of that group still runs.", aliases = List("-stop")) // backward compat val YstopBefore: Setting[List[String]] = PhasesSetting(ForkSetting, "Ystop-before", "Stop before") // stop before erasure as long as we have not debugged it fully val YshowSuppressedErrors: Setting[Boolean] = BooleanSetting(ForkSetting, "Yshow-suppressed-errors", "Also show follow-on errors and warnings that are normally suppressed.") val YlogicalPackageLoading: Setting[Boolean] = BooleanSetting(ForkSetting, "Ylogical-package-loading", "Enable logical package loading. This will load the logical package structure by preparsing the source files to discover the package structure. To be used together with -sourcepath option.") diff --git a/compiler/src/dotty/tools/dotc/core/Phases.scala b/compiler/src/dotty/tools/dotc/core/Phases.scala index d145f56f51f1..07a373605679 100644 --- a/compiler/src/dotty/tools/dotc/core/Phases.scala +++ b/compiler/src/dotty/tools/dotc/core/Phases.scala @@ -75,7 +75,6 @@ object Phases { final def fusePhases(phasess: List[List[Phase]], phasesToSkip: List[String], stopBeforePhases: List[String], - stopAfterPhases: List[String], YCheckAfter: List[String])(using Context): List[Phase] = { val fusedPhases = ListBuffer[Phase]() var prevPhases: Set[String] = Set.empty @@ -90,7 +89,7 @@ object Phases { val filteredPhases = phasess.map(_.filter { p => try isEnabled(p) - finally stop |= stopBeforePhases.contains(p.phaseName) | stopAfterPhases.contains(p.phaseName) + finally stop |= stopBeforePhases.contains(p.phaseName) }) var i = 0 diff --git a/compiler/src/dotty/tools/dotc/interactive/Completion.scala b/compiler/src/dotty/tools/dotc/interactive/Completion.scala index 3615875bc478..2d9872d0effe 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Completion.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Completion.scala @@ -505,7 +505,11 @@ object Completion: if qual.symbol.is(Package) then directMemberCompletions(adjustedQual) else if qual.typeOpt.hasSimpleKind then + def safeExtensionCompletions = + try extensionCompletions(adjustedQual) + catch case _: TypeError => Map.empty implicitConversionMemberCompletions(adjustedQual) ++ + //safeExtensionCompletions ++ extensionCompletions(adjustedQual) ++ directMemberCompletions(adjustedQual) ++ namedTupleCompletions(adjustedQual) @@ -736,7 +740,7 @@ object Completion: interactiv.println(i"implicit conversion targets considered: ${conversions.toList}%, %") conversions } catch case ex: Exception => - logger.warning( + logger.fine( s"Exception when searching for implicit conversions:\n ${ex.getMessage()}\n${ex.getStackTrace().mkString("\n")}" ) Set.empty diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 2ba415f43b27..b47d9c2b42ee 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -573,10 +573,11 @@ class PlainPrinter(_ctx: Context) extends Printer { case hi => hi.derivesFromCapSet) /** Print capture variable bounds using `^` syntax. - * Plain CapSet lower bound and universal upper bound are elided. + * Plain CapSet lower bound, empty lower bound, and universal upper bound are elided. */ private def toTextCaptureVarBounds(lo: Type, hi: Type): Text = val loText = lo match + case CapturingType(_, refs: CaptureSet) if refs.elems.isEmpty && !ccVerbose => Text() // empty lower bound case CapturingType(_, refs) => " >: " ~ toTextCaptureSet(refs) case _ => Text() // plain CapSet = trivial lower bound val hiText = hi match diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index eb2a42cb7b7a..61d19be49f8f 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -179,7 +179,9 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { val (printPure, refsText) = if refs == null then (isPure, Str("")) else if isElidableUniversal(refs) then (false, Str("")) - else (isPure, toTextGeneralCaptureSet(refs)) + else refs match + case cs: CaptureSet if cs.isConst && cs.elems.isEmpty => (true, Str("")) + case _ => (isPure, toTextGeneralCaptureSet(refs)) arrow(isContextual, printPure) ~ refsText private def toTextFunction(args: List[Type], res: Type, fn: MethodType | AppliedType, diff --git a/docs/_docs/contributing/debugging/inspection.md b/docs/_docs/contributing/debugging/inspection.md index f1a60da9f905..5f5dc72cdb24 100644 --- a/docs/_docs/contributing/debugging/inspection.md +++ b/docs/_docs/contributing/debugging/inspection.md @@ -61,10 +61,10 @@ Sometimes you may want to stop the compiler after a certain phase, for example t knock-on errors from occurring from a bug in an earlier phase. Use the flag `-Ystop-after:` to prevent any phases executing afterwards. -> e.g. `-Vprint:` where `phase` is a miniphase, will print after -> the whole phase group is complete, which may be several miniphases after `phase`. -> Instead you can use `-Ystop-after: -Vprint:` to stop -> immediately after the miniphase and see the trees that you intended. +> Note: `-Ystop-after:` stops after the whole phase group containing +> `` (a MegaPhase fuses its mini-phases into one traversal). +> Combined with `-Vprint:`, the printed trees reflect the state at +> the end of the group, which may include mini-phases after ``. ## Printing TASTy of a Class diff --git a/repl/src/dotty/tools/repl/ReplCompiler.scala b/repl/src/dotty/tools/repl/ReplCompiler.scala index 38e4f0f51ca3..cb654bcaa594 100644 --- a/repl/src/dotty/tools/repl/ReplCompiler.scala +++ b/repl/src/dotty/tools/repl/ReplCompiler.scala @@ -8,9 +8,12 @@ import dotc.core.Contexts.* import dotc.core.CompilationUnitInfo import dotc.core.Decorators.* import dotc.core.Flags.* +import dotc.core.NameKinds.SimpleNameKind import dotc.core.Names.* import dotc.core.NameKinds.ReplAssignName -import dotc.core.Phases.Phase +import dotc.core.Phases.{Phase, checkCapturesPhase} +import dotc.core.Contexts.atPhase +import dotc.config.Feature import dotc.core.StdNames.* import dotc.core.Symbols.* import dotc.reporting.Diagnostic @@ -97,17 +100,63 @@ class ReplCompiler extends Compiler: end compile final def typeOf(expr: String)(using state: State): Either[List[Diagnostic], String] = - typeCheck(expr).map { (_, tpdTree) => - given Context = state.context - tpdTree.rhs match { - case Block(xs, _) => xs.last.tpe.widen.show - case _ => - """Couldn't compute the type of your expression, so sorry :( - | - |Please report this to my masters at github.com/lampepfl/dotty - """.stripMargin + if Feature.ccEnabledSomewhere(using state.context) && checkCapturesPhase(using state.context).exists then + typeOfWithCC(expr) + else + typeCheck(expr).map { (_, tpdTree) => + given Context = state.context + tpdTree.rhs match { + case Block(xs, _) => xs.last.tpe.widen.show + case _ => + """Couldn't compute the type of your expression, so sorry :( + | + |Please report this to my masters at github.com/lampepfl/dotty + """.stripMargin + } } - } + + /** Compute the type of `expr` using the full compilation pipeline, + * until the capture checking phases. This ensures that the + * displayed type reflects capture annotations. + */ + private def typeOfWithCC(expr: String)(using state: State): Either[List[Diagnostic], String] = + val src = SourceFile.virtual(str.REPL_SESSION_LINE + (state.objectIndex + 1), expr) + ParseResult(src) match + case parsed: Parsed => + // Stop after CC — we only need types, not bytecode. + val ccCtx = state.context.fresh + .setSource(parsed.source) + .setSetting(state.context.settings.YstopAfter, List("cc")) + val compileState = state.copy(context = ccCtx) + compile(parsed)(using compileState).fold( + (errs, _) => Left(errs), + (unit, newState) => + given Context = newState.context + atPhase(checkCapturesPhase) { + // Find the result val in the wrapper module + val wrapperName = (str.REPL_SESSION_LINE + newState.objectIndex).toTermName + val wrapperSym = defn.RootClass.info.member(nme.EMPTY_PACKAGE).symbol + .info.member(wrapperName).symbol + if wrapperSym.exists then + val fields = wrapperSym.info.fields + .filterNot(_.symbol.isOneOf(ParamAccessor | Private | Synthetic | Artifact | Module)) + .filter(_.symbol.name.is(SimpleNameKind)) + fields.lastOption match + case Some(field) => Right(field.symbol.info.widen.show) + case None => + Left(List(new Diagnostic.Error( + s"Couldn't compute the type of your expression", + src.atSpan(Span(0, expr.length))))) + else + Left(List(new Diagnostic.Error( + s"Couldn't compute the type of your expression", + src.atSpan(Span(0, expr.length))))) + } + ) + case SyntaxErrors(_, errs, _) => Left(errs) + case _ => Left(List(new Diagnostic.Error( + s"Couldn't parse '$expr' to valid scala", + src.atSpan(Span(0, expr.length))))) def docOf(expr: String)(using state: State): Either[List[Diagnostic], String] = inContext(state.context) { diff --git a/repl/src/dotty/tools/repl/ReplDriver.scala b/repl/src/dotty/tools/repl/ReplDriver.scala index 7fd7aca454ac..bb6de3853df2 100644 --- a/repl/src/dotty/tools/repl/ReplDriver.scala +++ b/repl/src/dotty/tools/repl/ReplDriver.scala @@ -2,6 +2,7 @@ package dotty.tools package repl import scala.language.unsafeNulls +import scala.util.control.NonFatal import java.io.{File => JFile, PrintStream} import java.nio.charset.StandardCharsets @@ -45,7 +46,6 @@ import scala.collection.mutable import scala.compiletime.uninitialized import scala.jdk.CollectionConverters.* import scala.tools.asm.ClassReader -import scala.util.control.NonFatal import scala.util.Using /** The state of the REPL contains necessary bindings instead of having to have @@ -650,10 +650,13 @@ class ReplDriver(settings: Array[String], expr match { case "" => out.println(s":type ") case _ => - compiler.typeOf(expr)(using newRun(state)).fold( - errs => displayErrors(errs, state), - res => out.println(res) // result has some highlights - ) + try + compiler.typeOf(expr)(using newRun(state)).fold( + errs => displayErrors(errs, state), + res => out.println(res) // result has some highlights + ) + catch case NonFatal(ex) => + out.println(s"Error: ${ex.getMessage}") } state @@ -661,10 +664,13 @@ class ReplDriver(settings: Array[String], expr match { case "" => out.println(s":doc ") case _ => - compiler.docOf(expr)(using newRun(state)).fold( - errs => displayErrors(errs, state), - res => out.println(res) - ) + try + compiler.docOf(expr)(using newRun(state)).fold( + errs => displayErrors(errs, state), + res => out.println(res) + ) + catch case NonFatal(ex) => + out.println(s"Error: ${ex.getMessage}") } state diff --git a/repl/test/dotty/tools/repl/ReplCompilerTests.scala b/repl/test/dotty/tools/repl/ReplCompilerTests.scala index 1a08329fcfd3..36ce5848e7b0 100644 --- a/repl/test/dotty/tools/repl/ReplCompilerTests.scala +++ b/repl/test/dotty/tools/repl/ReplCompilerTests.scala @@ -662,6 +662,126 @@ class ReplCompilerTests extends ReplTest: "// defined class Ref\nlazy val mkRef: () -> Ref^{fresh}", storedOutput().trim) + @Test def `cc uses clause on nested class`: Unit = + initially: + run("import language.experimental.captureChecking") + .andThen: + storedOutput() // discard + // Inner class uses the outer's capability — should be able to call methods on it + run("class Outer(val io: AnyRef^) { class Inner uses io { def doIt: String = io.toString } }") + assertEquals("// defined class Outer", storedOutput().trim) + + @Test def `cc uses clause with initially`: Unit = + initially: + run("import language.experimental.captureChecking") + run("import caps.SharedCapability") + .andThen: + storedOutput() // discard + // `uses c initially` means c is only available in the constructor, not in methods + run("class Stream extends SharedCapability { def println(): Unit = () }") + .andThen: + storedOutput() // discard + run("object Console extends SharedCapability { val out: Stream = new Stream(); def println(): Unit = out.println() }") + .andThen: + storedOutput() // discard + // uses Console initially: constructor can use Console, methods cannot + run("object Greeter uses Console initially { Console.println(); def greet(): Unit = () }") + assertEquals("// defined object Greeter", storedOutput().trim) + + @Test def `cc Mutable class with update def`: Unit = + initially: + run("import language.experimental.captureChecking") + run("import caps.Mutable") + .andThen: + storedOutput() // discard + run("class Ref(init: Int) extends Mutable { private var current = init; def get: Int = current; update def put(x: Int): Unit = current = x }") + assertEquals( + "// defined class Ref", + storedOutput().trim) + + @Test def `cc Mutable class pretty print update method`: Unit = + initially: + run("import language.experimental.captureChecking") + run("import caps.Mutable") + .andThen: + storedOutput() // discard + // Define a Mutable class and a function that takes a mutable ref + run("class Ref(init: Int) extends Mutable { private var current = init; def get: Int = current; update def put(x: Int): Unit = current = x }") + .andThen: + storedOutput() // discard + run("def setRef(r: Ref^)(v: Int): Unit = r.put(v)") + assertEquals( + "def setRef(r: Ref^)(v: Int): Unit", + storedOutput().trim) + + @Test def `cc consume def on method`: Unit = + initially: + run("import language.experimental.captureChecking") + run("import caps.Mutable") + .andThen: + storedOutput() // discard + run("trait Sink extends Mutable { consume def close: Unit }") + assertEquals( + "// defined trait Sink", + storedOutput().trim) + + @Test def `cc type member Cap^`: Unit = + initially: + run("import language.experimental.captureChecking") + .andThen: + storedOutput() // discard + run("trait Reactor { type Cap^; def handler: () ->{Cap} Unit }") + assertEquals( + "// defined trait Reactor", + storedOutput().trim) + + @Test def `cc pretty print consume def on method`: Unit = + initially: + run("import language.experimental.captureChecking") + run("import caps.Mutable") + .andThen: + storedOutput() // discard + run("trait Sink extends Mutable { consume def close: Unit; consume def transfer: Sink^ }") + .andThen: + assertEquals("// defined trait Sink", storedOutput().trim) + run("def closeSink(consume s: Sink^): Unit = s.close") + assertEquals("def closeSink(consume s: Sink^): Unit", storedOutput().trim) + + @Test def `cc pretty print type member Cap^`: Unit = + initially: + run("import language.experimental.captureChecking") + .andThen: + storedOutput() // discard + run("trait Reactor { type Cap^; def handler: () ->{Cap} Unit }") + assertEquals( + "// defined trait Reactor", + storedOutput().trim) + + @Test def `cc pretty print ExclusiveCapability with consume val`: Unit = + initially: + run("import language.experimental.captureChecking") + run("import caps.{Mutable, ExclusiveCapability}") + .andThen: + storedOutput() // discard + run("class Ref(init: Int) extends Mutable { private var current = init; def get: Int = current; update def put(x: Int): Unit = current = x }") + .andThen: + storedOutput() // discard + run("class Owned(consume val inner: Ref^) extends ExclusiveCapability") + assertEquals( + "// defined class Owned", + storedOutput().trim) + + @Test def `cc separation checking accepts valid code`: Unit = + initially: + run("import language.experimental.separationChecking") + .andThen: + storedOutput() // discard + // separationChecking implies captureChecking; verify CC syntax works + run("def foo[C^](x: AnyRef^{C}): AnyRef^{x} = x") + assertEquals( + "def foo[C^](x: AnyRef^{C}): AnyRef^{x}", + storedOutput().trim) + @Test def `i16250 nested global language imports error`: Unit = initially: for feature <- List("captureChecking", "pureFunctions", "separationChecking", "safe") do run(s"def test = { import language.experimental.$feature; 1 }") diff --git a/repl/test/dotty/tools/repl/TabcompleteTests.scala b/repl/test/dotty/tools/repl/TabcompleteTests.scala index 0541e25b2992..e11060c81074 100644 --- a/repl/test/dotty/tools/repl/TabcompleteTests.scala +++ b/repl/test/dotty/tools/repl/TabcompleteTests.scala @@ -246,4 +246,18 @@ class TabcompleteTests extends ReplTest { @Test def i9334 = initially { assert(tabComplete("class Foo[T]; classOf[Foo].").contains("getName")) } + + // i25790: tab completion with CC enabled + // i25790: tab completion with CC enabled + @Test def `i25790 cc tab complete` = + initially { + run("import language.experimental.captureChecking") + } andThen { + storedOutput() + run("val x = new Object") + } andThen { + storedOutput() + val comp = tabComplete("x.toStr") + assertEquals(List("toString"), comp.distinct) + } } diff --git a/repl/test/dotty/tools/repl/TypeTests.scala b/repl/test/dotty/tools/repl/TypeTests.scala index a1d63d3595a0..8be8919770bc 100644 --- a/repl/test/dotty/tools/repl/TypeTests.scala +++ b/repl/test/dotty/tools/repl/TypeTests.scala @@ -28,3 +28,153 @@ class TypeTests extends ReplTest: run(":type") assertEquals(":type ", storedOutput().trim) } + + // scala/scala3#25465: :type on a type (not a term) should give a proper error, not crash + @Test def `i25465 type command with CC enabled` = + initially { + run("import language.experimental.captureChecking") + } andThen { + storedOutput() // discard + run("trait Foo") + } andThen { + storedOutput() // discard + run(":type Foo") + val output = storedOutput().trim + assertTrue(s"Expected error about term/type mismatch, got: $output", + output.contains("Not Found Error") || output.contains("Expected a term")) + } + + // scala/scala3#25465: :type with ill-formed expression should give proper error with CC + @Test def `i25465 type command ill-formed expr with CC` = + initially { + run("import language.experimental.captureChecking") + } andThen { + storedOutput() // discard + run(":type def foo[D](x: D): C = x") + val output = storedOutput().trim + // Should produce an error, not crash + assertTrue(s"Expected an error message, got: $output", output.nonEmpty) + } + + // scala/scala3#25465: :type should show capture annotations when CC is enabled + @Test def `i25465 type command shows captures` = + initially { + run("import language.experimental.captureChecking") + } andThen { + storedOutput() // discard + run("val x = new Object") + } andThen { + storedOutput() // discard + run(":type x") + val output = storedOutput().trim + assertTrue(s"Expected a type, got: $output", + output.nonEmpty && !output.contains("error")) + } + + // Type inference for capabilities in the REPL should work without + // explicit type annotations, even though vals are wrapped in objects. + @Test def `cc type inference for capabilities` = + initially { + run("import language.experimental.captureChecking") + run("import caps.*") + } andThen { + storedOutput() + run("class File extends SharedCapability") + } andThen { + assertEquals("// defined class File", storedOutput().trim) + run(":type File()") + } andThen { + assertEquals("File^", storedOutput().trim) + run("val x = File()") + } andThen { + assertTrue(storedOutput().trim.startsWith("val x: File^")) + run("class Logger(f: File^) extends SharedCapability") + } andThen { + assertEquals("// defined class Logger", storedOutput().trim) + run("val l = Logger(x)") + } andThen { + val lOut = storedOutput().trim + assertTrue(s"expected Logger with captures, got: $lOut", + lOut.contains("Logger") && lOut.contains("{") && lOut.contains("x")) + run(":type l") + } andThen { + assertEquals("Logger{val f: File^{x}}^{l}", storedOutput().trim) + run(":type Logger(x)") + assertEquals("Logger{val f: File^{x}}^{any, x}", storedOutput().trim) + } + + // :type should display inferred capture sets on function types + @Test def `cc type command shows capture set on function` = + initially { + run("import language.experimental.captureChecking") + } andThen { + storedOutput() // discard + run("def mkFn(a: AnyRef^) = () => a.toString()") + } andThen { + storedOutput() // discard + run(":type mkFn(new Object)") + assertEquals("() -> String", storedOutput().trim) + } + + // :type should display read-only captures (.rd) + @Test def `cc type command shows rd capture` = + initially { + run("import language.experimental.captureChecking") + run("import caps.Mutable") + } andThen { + storedOutput() + run("class Ref(init: Int) extends Mutable { private var current = init; def get: Int = current; update def put(x: Int): Unit = current = x }") + } andThen { + storedOutput() + run("def readRef(r: Ref^{caps.any.rd}): Int = r.get") + } andThen { + storedOutput() + // :type on a method reference shows its eta-expanded function type + run(":type readRef") + assertEquals("Ref^{any.rd} -> Int", storedOutput().trim) + } + + // :type should display classifier-restricted captures (.only[...]) + @Test def `cc type command shows classifier` = + initially { + run("import language.experimental.captureChecking") + run("import caps.{SharedCapability, Classifier}") + } andThen { + storedOutput() + run("trait Control extends SharedCapability, Classifier") + } andThen { + storedOutput() + run("def restricted(f: () ->{caps.any.only[Control]} Unit): Unit = f()") + } andThen { + storedOutput() + run(":type restricted") + assertEquals("(() ->{any.only[Control]} Unit) -> Unit", storedOutput().trim) + } + + // :type should display capture sets on values + @Test def `cc type command shows captures on value` = + initially { + run("import language.experimental.captureChecking") + } andThen { + storedOutput() + run("def withCaps(a: AnyRef^, b: AnyRef^): AnyRef^{a, b} = a") + } andThen { + storedOutput() + run(":type withCaps") + assertEquals("(a: AnyRef^, b: AnyRef^) -> Object^{a, b}", storedOutput().trim) + } + + // scala/scala3#25465: :doc on a type should give a proper error with CC, not crash + @Test def `i25465 doc command with CC enabled` = + initially { + run("import language.experimental.captureChecking") + } andThen { + storedOutput() // discard + run("trait Foo") + } andThen { + storedOutput() // discard + run(":doc Foo") + val output = storedOutput().trim + assertTrue(s"Expected error about term/type mismatch, got: $output", + output.contains("Not Found Error") || output.contains("Expected a term")) + } diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check index eb7ce4437cfa..480d8176fa6c 100644 --- a/tests/neg-custom-args/captures/capt1.check +++ b/tests/neg-custom-args/captures/capt1.check @@ -49,7 +49,7 @@ 38 | val z3 = h[(() -> Cap) @retains[x.type]](() => x)(() => C()) // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | Separation failure: Illegal access to {x} which is hidden by the previous definition - | of value z2 with type () ->{} C^. + | of value z2 with type () -> C^. | This type hides capabilities {any, x} | | where: ^ refers to a root capability in the type of value z2 diff --git a/tests/neg-custom-args/captures/levels.check b/tests/neg-custom-args/captures/levels.check index f4038941ef85..baf69c965828 100644 --- a/tests/neg-custom-args/captures/levels.check +++ b/tests/neg-custom-args/captures/levels.check @@ -1,7 +1,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/levels.scala:19:33 --------------------------------------- 19 | val _: Ref[String -> String] = r // error | ^ - | Found: Ref[String ->{} String]^{r.rd} + | Found: Ref[String -> String]^{r.rd} | Required: Ref[String -> String] | | Note that capability `r.rd` cannot flow into capture set {}. diff --git a/tests/neg-custom-args/captures/scope-extrusions.check b/tests/neg-custom-args/captures/scope-extrusions.check index 8639157cd3de..0465ae07edd1 100644 --- a/tests/neg-custom-args/captures/scope-extrusions.check +++ b/tests/neg-custom-args/captures/scope-extrusions.check @@ -142,7 +142,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/scope-extrusions.scala:33:21 ----------------------------- 33 | val f4: IO => IO = f3 // error | ^^ - | Found: (f3 : (x$0: IO) ->{} IO^{x$0}) + | Found: (f3 : (x$0: IO) -> IO^{x$0}) | Required: IO^ => IO^{any} | | Note that capability `x$0` cannot flow into capture set {any} diff --git a/tests/neg-custom-args/captures/sep-curried-par.check b/tests/neg-custom-args/captures/sep-curried-par.check index ecafd80773f6..1aa50d1bd62b 100644 --- a/tests/neg-custom-args/captures/sep-curried-par.check +++ b/tests/neg-custom-args/captures/sep-curried-par.check @@ -31,7 +31,7 @@ 21 | foo(c)(c) // error: separation | ^ |Separation failure: argument of type (c : () => Unit) - |to a function of type (x$0: () => Unit) ->{} (() ->{c, any} Unit) ->{x$0} Unit + |to a function of type (x$0: () => Unit) -> (() ->{c, any} Unit) ->{x$0} Unit |corresponds to capture-polymorphic formal parameter x$0 of type () =>² Unit |and hides capabilities {c}. |Some of these overlap with the captures of the function result with type (() ->{c, any} Unit) ->{c} Unit. diff --git a/tests/neg-custom-args/captures/use-capset.check b/tests/neg-custom-args/captures/use-capset.check index 0ab35941c85d..7978eadd7814 100644 --- a/tests/neg-custom-args/captures/use-capset.check +++ b/tests/neg-custom-args/captures/use-capset.check @@ -10,7 +10,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/use-capset.scala:13:50 ----------------------------------- 13 | val _: () -> List[Object^{io}] -> Object^{io} = h2 // error, should be ->{io} | ^^ - | Found: (h2 : () ->{} List[Object^{io}] ->{io} Object^{io}) + | Found: (h2 : () -> List[Object^{io}] ->{io} Object^{io}) | Required: () -> List[Object^{io}] -> Object^{io} | | Note that capability `io` cannot flow into capture set {}.