Skip to content

CC & REPL: restore :type, :doc, and tab completions#25789

Merged
bracevac merged 7 commits intoscala:mainfrom
dotty-staging:ob/fix-25465
May 4, 2026
Merged

CC & REPL: restore :type, :doc, and tab completions#25789
bracevac merged 7 commits intoscala:mainfrom
dotty-staging:ob/fix-25465

Conversation

@bracevac
Copy link
Copy Markdown
Contributor

@bracevac bracevac commented Apr 14, 2026

Fix #25465
Fix #25790

Problem

In a REPL session with capture checking enabled:

  • :type and :doc crash (ArrayIndexOutOfBoundsException) instead of reporting a normal error
  • tab completion fails for ordinary lookups such as x.toStr
  • REPL wrapper vals spuriously trip the explicit-type check for fields that capture a root capability
scala> import language.experimental.captureChecking
scala> trait Foo
scala> :type Foo
java.lang.ArrayIndexOutOfBoundsException: ...

Root cause

ReplCompiler.typeCheck runs compileUnits with -Ystop-after:typer. Until now, that truncated the shared phase tables — later REPL helpers consulting CC phase ids would blow up. Tab completion separately re-entered Predef's wrap*Array implicits during scope scanning. And the explicit-type check fired on synthetic REPL wrapper fields that the user can never see.

Fix

  1. Decouple -Ystop-after from phase registration. fusePhases no longer takes a stopAfterPhases parameter; the registered phase plan is always full and phase ids stay stable. runPhases honors stopAfter at execution time and stops after the phase group containing the named phase. The setting description and the inspection docs are updated to reflect that group-level granularity.
  2. REPL typeOfWithCC runs the full pipeline up to and including cc, so :type shows capture annotations.
  3. Exempt REPL wrapper fields from the explicit-type check for fields capturing root capabilities — those wrappers are invisible to the user.
  4. Suppress TypeError from extensionCompletions in the REPL so cycles in Predef's wrap*Array don't kill tab completion.
  5. Printer: empty capture sets on function arrows (->{}) print as ->; trivial empty lower bounds on capture-set type parameters (>: {}) are elided.

Tests

  • CC-enabled tab completion (#25790)
  • :type on CC examples (Logger / File capabilities, .rd, classifiers, function captures, eta-expanded methods)
  • REPL wrapper inference / explicit-type handling for capability-typed vals
  • Updated sep-curried-par.check for the ->{}-> printer change

How much have you relied on LLM-based tools in this contribution?

Lots

How was the solution tested?

  • Full REPL test suite (sbt scala3-repl/test) passes
  • Manual REPL testing with import language.experimental.captureChecking, then :type, :doc, and tab-completion checks

@bracevac bracevac force-pushed the ob/fix-25465 branch 5 times, most recently from e4c0392 to 5480695 Compare April 14, 2026 23:17
@bracevac bracevac changed the title Fix #25465: CC breaks REPL :type and :doc commands CC & REPL: restore :type, :doc, and tab completions Apr 14, 2026
@bracevac
Copy link
Copy Markdown
Contributor Author

bracevac commented Apr 14, 2026

While this is strictly better now, we still have some usability warts, which I suspect are due to the way the REPL wraps lines under the hood:

Type inference is broken

scala> val l = Logger(y)
-- Error: ----------------------------------------------------------------------
1 |val l = Logger(y)
  |     ^
  |value l needs an explicit type because it captures a root capability in its type Logger{val f: File^{y}}^{any, y}.
  |Fields capturing a root capability need to be given an explicit type unless the capability is already
  |subsumed by the computed capability of the enclosing class.
  |
  |where:    any is a root capability classified as SharedCapability in the type of value l
1 error found

And say we have managed to define l, then the :type command also complains:

scala> val l:Logger^{caps.any,y} = Logger(y)
val l: Logger^{any, y} = Logger@52bf434b

scala> :type l
-- Error: ----------------------------------------------------------------------
1 |l
  |^
  |value res0 needs an explicit type because it captures a root capability in its type Logger{val f: File^}^{l}.
  |Fields capturing a root capability need to be given an explicit type unless the capability is already
  |subsumed by the computed capability of the enclosing class.
  |
  |where:    ^ refers to a root capability classified as SharedCapability in the type of value f

Fairly sure this is an artifact of how we treat classes/objects and the REPL wrapping lines in them.

Edit: After rebasing with the latest CC changes, these issues seem to have been resolved. However, there's still issues around type inference, e.g.,

scala> import language.experimental.captureChecking

scala> import caps.*

scala> class File extends SharedCapability

scala> val x = File()
-- Error: ----------------------------------------------------------------------
1 |val x = File()
  |     ^
  |value x needs an explicit type because it captures a root capability in its type File^.
  |Fields capturing a root capability need to be given an explicit type unless the capability is already
  |subsumed by the computed capability of the enclosing class.
  |
  |where:    ^ refers to a root capability classified as SharedCapability in the type of value x
1 error found

scala> :type File()
-- Error: ----------------------------------------------------------------------
1 |File()
  |^
  |value res0 needs an explicit type because it captures a root capability in its type File^.
  |Fields capturing a root capability need to be given an explicit type unless the capability is already
  |subsumed by the computed capability of the enclosing class.
  |
  |where:    ^ refers to a root capability classified as SharedCapability in the type of value res0
1 error found

These are a bit too surprising for users IMO and should actually work.

@bracevac bracevac force-pushed the ob/fix-25465 branch 4 times, most recently from ddd92fa to 7cff2d4 Compare April 15, 2026 14:49
@bracevac
Copy link
Copy Markdown
Contributor Author

Edit: After rebasing with the latest CC changes, these issues seem to have been resolved. However, there's still issues around type inference, e.g.,

// [...]
scala> val x = File()
// [...]
scala> :type File()

These work now.

"// defined class Ref\nlazy val mkRef: () -> Ref^{fresh}",
storedOutput().trim)

@Test def `cc uses clause on nested class`: Unit =
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's easier to write repl tests in repl/test-resources/repl/?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scripted test format doesn't work for our tests because:

  1. :type and :doc commands get parsed as Scala code, not REPL commands
  2. val output includes non-deterministic object addresses
  3. FileDiff.matches is strict equality — no wildcards

@bracevac
Copy link
Copy Markdown
Contributor Author

Here's something that's outside the scope of this PR:

The :type command gives hilariously bad types for eta-expanded methods.

scala> def foo[X](id: X): X = id
def foo[X](id: X): X

scala> :type foo
Any -> Any

Involving captures:

// assume val l = Logger(f) in context
scala> def convert2(xs: List[Unit]): List[Unit] = { println(l); xs.map(_ => ()) }
def convert2(xs: List[Unit]): List[Unit]

scala> :type convert2
List[Unit] ->{rs$line$16} List[Unit]

The capture set will mention the synthetic wrapper object of l, not l itself.

I also discovered a genuine bug (#25830) in the type inference of polymorphic lambdas:

scala> val convert22 = { [C^] => (xs: List[File^{C}]) =>  println(l); xs.map(_ => ()) }
val convert22: [C^] => (xs: List[File^{}]) ->{l} List[Unit] = Lambda$2370/0x000000c8015fefc8@6ae6a1ce

It should be (xs: List[File^{C}).

@noti0na1
Copy link
Copy Markdown
Member

This PR passes all tests in tacit as well.

@bracevac bracevac force-pushed the ob/fix-25465 branch 3 times, most recently from c7c8949 to f6cb2ff Compare April 22, 2026 14:27
@bracevac
Copy link
Copy Markdown
Contributor Author

@odersky the changes to SymDenotations break lots of things in the CI. Just checking for existence of the denotation is futile, as it's already too late at that point.
Maybe the previous fix bc808c1 (which is local to the REPL) is better?

@bracevac bracevac force-pushed the ob/fix-25465 branch 4 times, most recently from bf760af to b817c60 Compare April 24, 2026 15:50
@bracevac
Copy link
Copy Markdown
Contributor Author

@odersky as per our discussion, I reverted to the simple strategy of suppressing the cycle error from extensionCompletions in the REPL. From my end, this is ready now.

@bracevac
Copy link
Copy Markdown
Contributor Author

bracevac commented Apr 24, 2026

For posterity: with CC enabled, the tab completion can print weird cyclicity errors from resolving extension methods. Those errors relate to the various wrap*Array implicits in Predef. Those seem to involve an unexpected ClassCastException when resolving package objects in SymDenotations.

Several things were tried, including unlinking cc symdenotation transformers, and recovering the denotation state on such unexpected errors. But those were deemed too risky.

In the end, we decided to just gracefully recover in the REPL when the completion of extension methods produces such an error.

Copy link
Copy Markdown
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise LGTM

config.println(s"nextDenotTransformerId = ${nextDenotTransformerId.toList}")
}

/** Save the phase arrays so they can be restored after a `compileUnits`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a bit pedestrian. I think it would overall be simpler if runPhases stopped after stopAfter and the phase state did not depend on Ystop-after. The we would not need to save and restore phase state.

@odersky odersky assigned bracevac and unassigned odersky May 3, 2026
Comment thread compiler/src/dotty/tools/dotc/Run.scala Outdated
var i = 0
while i < allPhases.length && !stopped do
val phase = allPhases(i) match
case mp: dotty.tools.dotc.transform.MegaPhase => mp.truncatedAt(stopAfter, ctx.base)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need the truncatedAt functionality. Let's just run the full megaphase. That simplifies things.

bracevac added 6 commits May 4, 2026 12:26
…pletions

The REPL's typeCheck method (used by :type, :doc, and tab completions)
calls compileUnits with YstopAfter="typer", which truncates the shared
base.phases array. When subsequent code accesses phase IDs from a
previous full compilation, it crashes with ArrayIndexOutOfBoundsException.

Three fixes:

1. Save and restore the phase arrays around the truncating compileUnits
   call in typeCheck, so that tab completions, error rendering (DidYouMean),
   and other subsequent operations see the full phases.

2. When capture checking is enabled, :type now uses the full compile
   pipeline (including CC phases) instead of typeCheck (typer-only),
   so that displayed types correctly reflect capture annotations.

3. Fix printing of function types with const empty capture sets:
   () ->{} T now prints as () -> T (pure), since an empty capture
   set is equivalent to purity.

Also adds comprehensive REPL tests for CC features: uses/initially
clauses, Mutable/update def, consume def, type Cap^ members,
ExclusiveCapability, separation checking, tab completion with CC,
and :type with .rd, .only[Classifier], and capture set annotations.
extensionCompletions can throw CyclicReference (a TypeError) during
scope scanning when CC is enabled. This kills the entire completion
pipeline since the exception propagates past directMemberCompletions.
Fix by catching TypeError in extensionCompletions so direct member
completions still work.

Also prevent :type and :doc from crashing the REPL on uncaught
exceptions by adding error handling in the command handlers.

Downgrade implicit conversion search exception log from WARNING to
FINE (debug level) to avoid noisy output during tab completion.
REPL wrapper objects are invisible to the user and should not
trigger "needs explicit type" errors for capability-typed vals.
Add tests for type inference of SharedCapability instances and
Logger with captures in the REPL.
Decouple `-Ystop-after` from phase registration: `fusePhases` no longer
takes a `stopAfterPhases` parameter, so the registered phase array is
always the full plan with stable IDs. `runPhases` honors `stopAfter`
at execution time — for MegaPhases spanning the stop boundary,
`MegaPhase.truncatedAt` builds a fresh prefix MegaPhase reusing the
already-init'd mini-phases.

This removes the need for `savePhaseState`/`restorePhaseState` and
the wrappers around `compileUnits` in the REPL.
@bracevac bracevac merged commit baa7ff6 into scala:main May 4, 2026
45 checks passed
@bracevac bracevac deleted the ob/fix-25465 branch May 4, 2026 16:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CC: Tab completions broken in REPL Capture checking breaks the REPL/scala-cli :type command

3 participants