CC & REPL: restore :type, :doc, and tab completions#25789
CC & REPL: restore :type, :doc, and tab completions#25789bracevac merged 7 commits intoscala:mainfrom
Conversation
e4c0392 to
5480695
Compare
|
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 foundAnd say we have managed to define 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 fFairly 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 foundThese are a bit too surprising for users IMO and should actually work. |
ddd92fa to
7cff2d4
Compare
These work now. |
| "// defined class Ref\nlazy val mkRef: () -> Ref^{fresh}", | ||
| storedOutput().trim) | ||
|
|
||
| @Test def `cc uses clause on nested class`: Unit = |
There was a problem hiding this comment.
Maybe it's easier to write repl tests in repl/test-resources/repl/?
There was a problem hiding this comment.
The scripted test format doesn't work for our tests because:
:typeand:doccommands get parsed as Scala code, not REPL commandsvaloutput includes non-deterministic object addresses- FileDiff.matches is strict equality — no wildcards
|
Here's something that's outside the scope of this PR: The scala> def foo[X](id: X): X = id
def foo[X](id: X): X
scala> :type foo
Any -> AnyInvolving 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 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@6ae6a1ceIt should be |
|
This PR passes all tests in tacit as well. |
c7c8949 to
f6cb2ff
Compare
bf760af to
b817c60
Compare
|
@odersky as per our discussion, I reverted to the simple strategy of suppressing the cycle error from |
|
For posterity: with CC enabled, the tab completion can print weird cyclicity errors from resolving extension methods. Those errors relate to the various 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. |
| config.println(s"nextDenotTransformerId = ${nextDenotTransformerId.toList}") | ||
| } | ||
|
|
||
| /** Save the phase arrays so they can be restored after a `compileUnits` |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
I don't think we need the truncatedAt functionality. Let's just run the full megaphase. That simplifies things.
…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.
Fix #25465
Fix #25790
Problem
In a REPL session with capture checking enabled:
:typeand:doccrash (ArrayIndexOutOfBoundsException) instead of reporting a normal errorx.toStrRoot cause
ReplCompiler.typeCheckrunscompileUnitswith-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'swrap*Arrayimplicits during scope scanning. And the explicit-type check fired on synthetic REPL wrapper fields that the user can never see.Fix
-Ystop-afterfrom phase registration.fusePhasesno longer takes astopAfterPhasesparameter; the registered phase plan is always full and phase ids stay stable.runPhaseshonorsstopAfterat 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.typeOfWithCCruns the full pipeline up to and includingcc, so:typeshows capture annotations.TypeErrorfromextensionCompletionsin the REPL so cycles in Predef'swrap*Arraydon't kill tab completion.->{}) print as->; trivial empty lower bounds on capture-set type parameters (>: {}) are elided.Tests
#25790):typeon CC examples (Logger / File capabilities,.rd, classifiers, function captures, eta-expanded methods)sep-curried-par.checkfor the->{}→->printer changeHow much have you relied on LLM-based tools in this contribution?
Lots
How was the solution tested?
sbt scala3-repl/test) passesimport language.experimental.captureChecking, then:type,:doc, and tab-completion checks