Skip to content

Commit df5f612

Browse files
authored
Allow global language imports in REPL and snippet compiler (scala#25458)
Previously, `import language.experimental.captureChecking` (and related global language imports) were rejected in the REPL and snippet compiler because they appeared in nested scopes rather than at the toplevel. This change takes a structural approach to fix both contexts: - REPL: Forward `outermost` through `blockStatSeq` in Interactive mode so the parser treats REPL-level imports as toplevel. After a successful compile, propagate global language imports to `rootCtx` as `-language:` settings so subsequent inputs can parse CC syntax (e.g. `^`). - Snippet compiler: Extract global language imports from snippet body and place them before the `object Snippet {}` wrapper, making them genuine toplevel imports. Fixes scala#16250 ## How much have your relied on LLM-based tools in this contribution? Used Claude extensively to analyze the problem and iterate on the solution. ## How was the solution tested? There are REPL and snippet compiler tests. I also verified manually within the REPL that the top-level CC import works. Updated some snippets in the CC language ref to verify that the snippet compilation works as well.
1 parent e5a1ae2 commit df5f612

File tree

16 files changed

+304
-70
lines changed

16 files changed

+304
-70
lines changed

compiler/src/dotty/tools/dotc/config/Feature.scala

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,10 +248,17 @@ object Feature:
248248
def isExperimentalEnabledByImport(using Context): Boolean =
249249
experimentalAutoEnableFeatures.exists(enabledByImport)
250250

251-
/** Handle language import `import language.<prefix>.<imported>` if it is one
252-
* of the global imports `pureFunctions` or `captureChecking`. In this case
253-
* make the compilation unit's and current run's fields accordingly.
254-
* @return true iff import that was handled
251+
/** Global language imports that affect parsing and must be handled specially.
252+
* These need per-compilation-unit flags (set in `handleGlobalLanguageImport`)
253+
* and also require propagation to rootCtx in the REPL and hoisting in the
254+
* snippet compiler so they take effect across inputs (i16250).
255+
*/
256+
val globalLanguageImports: Set[TermName] =
257+
Set(pureFunctions, captureChecking, separationChecking, safe)
258+
259+
/** Handle a global language import `import language.<prefix>.<imported>`.
260+
* Sets the compilation unit's and current run's fields accordingly.
261+
* @return true iff the import was handled
255262
*/
256263
def handleGlobalLanguageImport(prefix: TermName, imported: Name)(using Context): Boolean =
257264
QualifiedName(prefix, imported.asTermName) match

compiler/src/dotty/tools/dotc/parsing/Parsers.scala

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3937,15 +3937,7 @@ object Parsers {
39373937
in.languageImportContext = in.languageImportContext.importContext(imp, NoSymbol)
39383938
for case ImportSelector(id @ Ident(imported), EmptyTree, _) <- selectors do
39393939
if Feature.handleGlobalLanguageImport(prefix, imported) && !outermost then
3940-
val desc =
3941-
if ctx.mode.is(Mode.Interactive) then
3942-
"not allowed in the REPL"
3943-
else "only allowed at the toplevel"
3944-
val hint =
3945-
if ctx.mode.is(Mode.Interactive) then
3946-
f"\nTo use this language feature, include the flag `-language:$prefix.$imported` when starting the REPL"
3947-
else ""
3948-
syntaxError(em"this language import is $desc$hint", id.span)
3940+
syntaxError(em"this language import is only allowed at the toplevel", id.span)
39493941
if allSourceVersionNames.contains(imported) && prefix.isEmpty then
39503942
if !outermost then
39513943
syntaxError(em"source version import is only allowed at the toplevel", id.span)
@@ -5023,7 +5015,7 @@ object Parsers {
50235015
while
50245016
var empty = false
50255017
if (in.token == IMPORT)
5026-
stats ++= importClause()
5018+
stats ++= importClause(outermost = outermost && ctx.mode.is(Mode.Interactive))
50275019
else if (isExprIntro)
50285020
stats += expr(Location.InBlock)
50295021
else if in.token == IMPLICIT && !in.inModifierPosition() then

compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,8 @@ class PlainPrinter(_ctx: Context) extends Printer {
329329
~ Str("R").provided(printDebug)
330330
else toText(tpe)
331331
case annot: CaptureAnnotation =>
332-
toTextLocal(tpe) ~ "^" ~ toText(annot)
332+
val boxText: Text = Str("box ") `provided` annot.boxed && ccVerbose
333+
toTextCapturing(tpe, annot.refs, boxText)
333334
case _ if defn.SilentAnnots.contains(annot.symbol) && !printDebug =>
334335
toText(tpe)
335336
case _ =>
@@ -567,6 +568,25 @@ class PlainPrinter(_ctx: Context) extends Printer {
567568
(tparamStr, bounds.derivedTypeBounds(loRhs, hiRhs))
568569
end decomposeLambdas
569570

571+
/** Is this a capture variable's bounds? i.e. lo and hi are both CapSet-based. */
572+
private def isCaptureVarBounds(lo: Type, hi: Type): Boolean =
573+
lo.derivesFrom(defn.Caps_CapSet) && (hi match
574+
case CapturingType(parent, _) => parent.derivesFrom(defn.Caps_CapSet)
575+
case hi => hi.derivesFrom(defn.Caps_CapSet))
576+
577+
/** Print capture variable bounds using `^` syntax.
578+
* Plain CapSet lower bound and universal upper bound are elided.
579+
*/
580+
private def toTextCaptureVarBounds(lo: Type, hi: Type): Text =
581+
val loText = lo match
582+
case CapturingType(_, refs) => " >: " ~ toTextCaptureSet(refs)
583+
case _ => Text() // plain CapSet = trivial lower bound
584+
val hiText = hi match
585+
case CapturingType(_, refs) if isElidableUniversal(refs) => Text() // trivial upper bound
586+
case CapturingType(_, refs) => " <: " ~ toTextCaptureSet(refs)
587+
case _ => Text()
588+
Str("^") ~ loText ~ hiText
589+
570590
/** String representation of a definition's type following its name */
571591
protected def toTextRHS(tp: Type, isParameter: Boolean = false): Text = controlled {
572592
homogenize(tp) match {
@@ -575,6 +595,8 @@ class PlainPrinter(_ctx: Context) extends Printer {
575595
val binder = rhs match
576596
case tp: AliasingBounds =>
577597
" = " ~ toText(tp.alias)
598+
case TypeBounds(lo, hi) if !printDebug && Feature.ccEnabledSomewhere && isCaptureVarBounds(lo, hi) =>
599+
toTextCaptureVarBounds(lo, hi)
578600
case TypeBounds(lo, hi) =>
579601
(if lo.isExactlyNothing then Text() else " >: " ~ toText(lo))
580602
~ (if hi.isExactlyAny || (!printDebug && hi.isFromJavaObject) then Text() else " <: " ~ toText(hi))

compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,6 +1176,10 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
11761176
protected override def annotText(sym: Symbol): Text =
11771177
if sym == defn.ConsumeAnnot then "consume" else super.annotText(sym)
11781178

1179+
protected override def specialAnnotText(sym: ClassSymbol, tp: Type): Text =
1180+
if sym == defn.ConsumeAnnot then keywordText("consume ").provided(tp.hasAnnotation(sym))
1181+
else super.specialAnnotText(sym, tp)
1182+
11791183
protected def modText(mods: untpd.Modifiers, sym: Symbol, kw: String, isType: Boolean): Text = { // DD
11801184
val suppressKw = if (enclDefIsClass) mods.isAllOf(LocalParam) else mods.is(Param)
11811185
var flagMask =

docs/_docs/reference/experimental/capture-checking/basics.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/capture-che
77
## Introduction
88

99
```scala sc-hidden sc-name:cc-context
10+
import language.experimental.captureChecking
1011
import caps.*
1112
```
1213

docs/_docs/reference/experimental/capture-checking/mutability.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ title: "Stateful Capabilities"
44
nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/capture-checking/mutability.html
55
---
66

7+
```scala sc-hidden sc-name:mut-context
8+
import language.experimental.captureChecking
9+
import language.experimental.separationChecking
10+
```
11+
712
## Introduction
813

914
An important class of effects represents accesses to mutable variables and mutable data structures. This is intimately tied with the concept of _program state_. Stateful capabilities are capabilities that allow to consult and change the program state.
@@ -13,7 +18,7 @@ We distinguish two kinds of accesses: full access that allows state changes and
1318
A common kind of stateful capabilities represent mutable variables that can be read and written.
1419
These mutable data structures are expressed with the marker trait `caps.Mutable`.
1520
For instance, consider a simple reference cell:
16-
```scala
21+
```scala sc-compile-with:mut-context
1722
import caps.Mutable
1823

1924
class Ref[T](init: T) extends Mutable:
@@ -46,7 +51,7 @@ These classes typically contain mutable variables and/or _update methods_.
4651
Update methods are declared using a new soft modifier `update`.
4752

4853
**Example:**
49-
```scala
54+
```scala sc-compile-with:mut-context
5055
//{
5156
import caps.*
5257
//}

docs/_docs/reference/experimental/capture-checking/safe.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ Full Scala 3 has elements that are incompatible with capability safety, such as
1212

1313
To distinguish between these two usage modes, there is a safe language subset that can be specified with a command-line option or a language import:
1414
```scala sc:nocompile
15-
import language.experimental.safe
15+
import language.experimental.safe
16+
```
17+
18+
```scala sc-hidden sc-name:safe-context
19+
import language.experimental.safe
1620
```
1721

1822
It makes sense for agentic tooling to subject all compilations of agent-generated code to be compiled in _safe mode_ using this language import. Safe mode imposes the following restrictions:

project/Build.scala

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2003,10 +2003,7 @@ object Build {
20032003
.add(SnippetCompiler(List(
20042004
s"${docs.getAbsolutePath}/_docs/reference/new-types=compile",
20052005
s"${docs.getAbsolutePath}/_docs/reference/enums=compile",
2006-
s"$ccDocs=compile|-language:experimental.captureChecking",
2007-
s"$ccDocs/separation-checking=compile|-language:experimental.captureChecking|-language:experimental.separationChecking",
2008-
s"$ccDocs/mutability=compile|-language:experimental.captureChecking|-language:experimental.separationChecking",
2009-
s"$ccDocs/safe=compile|-language:experimental.safe",
2006+
s"$ccDocs=compile",
20102007
)))
20112008
}
20122009

@@ -2965,10 +2962,7 @@ object ScaladocConfigs {
29652962
snippetCompilerTargets(s"$stdlib/src") ++ List(
29662963
"docs/_docs/reference/new-types=compile",
29672964
"docs/_docs/reference/enums=compile",
2968-
"docs/_docs/reference/experimental/capture-checking=compile|-language:experimental.captureChecking",
2969-
"docs/_docs/reference/experimental/capture-checking/separation-checking=compile|-language:experimental.captureChecking|-language:experimental.separationChecking",
2970-
"docs/_docs/reference/experimental/capture-checking/mutability=compile|-language:experimental.captureChecking|-language:experimental.separationChecking",
2971-
"docs/_docs/reference/experimental/capture-checking/safe=compile|-language:experimental.safe",
2965+
"docs/_docs/reference/experimental/capture-checking=compile",
29722966
)))
29732967
.add(SiteRoot("docs"))
29742968
.add(ApiSubdirectory(true))

repl/src/dotty/tools/repl/ReplDriver.scala

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import dotc.config.CommandLineParser.tokenize
1313
import dotc.config.Properties.{javaVersion, javaVmName, simpleVersionString}
1414
import dotc.core.Contexts.*
1515
import dotc.core.Decorators.*
16-
import dotc.core.Phases.{unfusedPhases, typerPhase}
16+
import dotc.core.Phases.{unfusedPhases, typerPhase, checkCapturesPhase}
1717
import dotc.core.Denotations.Denotation
1818
import dotc.core.Flags.*
1919
import dotc.core.Mode
@@ -32,7 +32,7 @@ import dotc.reporting.Diagnostic
3232
import dotc.util.Spans.Span
3333
import dotc.util.{SourceFile, SourcePosition}
3434
import dotc.{CompilationUnit, Driver}
35-
import dotc.config.CompilerCommand
35+
import dotc.config.{CompilerCommand, Feature}
3636
import dotty.tools.io.{AbstractFileClassLoader => _, *}
3737
import dotty.tools.repl.ScalaClassLoader.*
3838

@@ -304,6 +304,25 @@ class ReplDriver(settings: Array[String],
304304
state.copy(context = run.runContext)
305305
}
306306

307+
/** Add a language feature to rootCtx so subsequent parses and compilations see it. */
308+
private def enableLanguageFeature(feature: String): Unit =
309+
val summary = rootCtx.settings.processArguments(List(s"-language:$feature"), true, rootCtx.settingsState)
310+
rootCtx = rootCtx.fresh.setSettings(summary.sstate)
311+
312+
/** Detect global language imports in parsed trees and enable them in rootCtx
313+
* so subsequent parses and compilations see them (i16250).
314+
*/
315+
private def propagateLanguageImports(trees: List[untpd.Tree]): Unit =
316+
import dotc.core.NameKinds.QualifiedName
317+
for case untpd.Import(expr, selectors) <- trees do
318+
untpd.languageImport(expr) match
319+
case Some(prefix) =>
320+
for case untpd.ImportSelector(untpd.Ident(imported), untpd.EmptyTree, _) <- selectors do
321+
val qual = QualifiedName(prefix, imported.asTermName)
322+
if Feature.globalLanguageImports.contains(qual) then
323+
enableLanguageFeature(qual.toString)
324+
case _ =>
325+
307326
private def stripBackTicks(label: String) =
308327
if label.startsWith("`") && label.endsWith("`") then
309328
label.drop(1).dropRight(1)
@@ -340,6 +359,7 @@ class ReplDriver(settings: Array[String],
340359
state
341360

342361
case parsed: Parsed if parsed.trees.nonEmpty =>
362+
propagateLanguageImports(parsed.trees)
343363
compile(parsed, state)
344364

345365
case SyntaxErrors(_, errs, _) =>
@@ -422,7 +442,7 @@ class ReplDriver(settings: Array[String],
422442
private def renderDefinitions(tree: tpd.Tree, newestWrapper: Name)(using state: State): (State, Seq[Diagnostic]) = {
423443
given Context = state.context
424444

425-
def resAndUnit(denot: Denotation) = {
445+
def resAndUnit(denot: Denotation)(using Context) = {
426446
import scala.util.{Success, Try}
427447
val sym = denot.symbol
428448
val name = sym.name.show
@@ -433,7 +453,7 @@ class ReplDriver(settings: Array[String],
433453
name.startsWith(str.REPL_RES_PREFIX) && hasValidNumber && sym.info == defn.UnitType
434454
}
435455

436-
def extractAndFormatMembers(symbol: Symbol): (State, Seq[Diagnostic]) = if (tree.symbol.info.exists) {
456+
def extractAndFormatMembers(symbol: Symbol)(using Context): (State, Seq[Diagnostic]) = if (tree.symbol.info.exists) {
437457
val info = symbol.info
438458
val defs =
439459
info.bounds.hi.finalResultType
@@ -484,13 +504,17 @@ class ReplDriver(settings: Array[String],
484504
def isSyntheticCompanion(sym: Symbol) =
485505
sym.is(Module) && sym.is(Synthetic)
486506

487-
def typeDefs(sym: Symbol): Seq[Diagnostic] = sym.info.memberClasses
507+
def typeDefs(sym: Symbol)(using Context): Seq[Diagnostic] = sym.info.memberClasses
488508
.collect {
489509
case x if !isSyntheticCompanion(x.symbol) && !x.symbol.name.isReplWrapperName =>
490510
rendering.renderTypeDef(x)
491511
}
492512

493-
atPhase(typerPhase.next) {
513+
val renderPhase =
514+
if Feature.ccEnabledSomewhere && checkCapturesPhase.exists
515+
then checkCapturesPhase
516+
else typerPhase.next
517+
atPhase(renderPhase) {
494518
// Display members of wrapped module:
495519
tree.symbol.info.memberClasses
496520
.find(_.symbol.name == newestWrapper.moduleClassName)

0 commit comments

Comments
 (0)