Skip to content

Commit 985919c

Browse files
committed
Allow global language imports in REPL and snippet compiler
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
1 parent 14d1bf9 commit 985919c

File tree

6 files changed

+116
-31
lines changed

6 files changed

+116
-31
lines changed

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

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,30 @@ 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+
/** Global language features that affect parsing and must be propagated to rootCtx (i16250). */
313+
private val globalLanguageFeatures = Set(
314+
"experimental.captureChecking", "experimental.pureFunctions",
315+
"experimental.separationChecking", "experimental.safe")
316+
317+
/** Detect global language imports in parsed trees and enable them as settings (i16250). */
318+
private def propagateLanguageImports(trees: List[untpd.Tree]): Unit =
319+
for tree <- trees do
320+
tree match
321+
case untpd.Import(expr, selectors) =>
322+
untpd.languageImport(expr) match
323+
case Some(prefix) =>
324+
for case untpd.ImportSelector(untpd.Ident(imported), untpd.EmptyTree, _) <- selectors do
325+
val qual = if prefix.isEmpty then imported.toString else s"$prefix.$imported"
326+
if globalLanguageFeatures.contains(qual) then
327+
enableLanguageFeature(qual)
328+
case _ =>
329+
case _ =>
330+
307331
private def stripBackTicks(label: String) =
308332
if label.startsWith("`") && label.endsWith("`") then
309333
label.drop(1).dropRight(1)
@@ -382,7 +406,16 @@ class ReplDriver(settings: Array[String],
382406
.fold(
383407
displayErrors,
384408
{
385-
case (unit: CompilationUnit, newState: State) =>
409+
case (unit: CompilationUnit, newState0: State) =>
410+
// Propagate global language imports to rootCtx so subsequent parses see them (i16250).
411+
// We check the parsed trees rather than the compilation unit flags because the REPL
412+
// parses in a separate step that uses a temporary compilation unit.
413+
propagateLanguageImports(parsed.trees)
414+
// Update the state's context settings to match rootCtx so subsequent parsing sees them
415+
val newState =
416+
if newState0.context.settingsState ne rootCtx.settingsState then
417+
newState0.copy(context = newState0.context.fresh.setSettings(rootCtx.settingsState))
418+
else newState0
386419
val newestWrapper = extractNewestWrapper(unit.untpdTree)
387420
val newImports = extractTopLevelImports(newState.context)
388421
var allImports = newState.imports

repl/test/dotty/tools/repl/ReplCompilerTests.scala

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -492,23 +492,53 @@ class ReplCompilerTests extends ReplTest:
492492
assertTrue(all.head.startsWith("-- [E103] Syntax Error"))
493493
assertTrue(all.exists(_.trim().startsWith("| Illegal start of statement: this modifier is not allowed here")))
494494

495-
@Test def `i16250a`: Unit = initially:
496-
val hints = List(
497-
"this language import is not allowed in the REPL",
498-
"To use this language feature, include the flag `-language:experimental.captureChecking` when starting the REPL"
499-
)
500-
run("import language.experimental.captureChecking")
501-
val all = lines()
502-
assertTrue(hints.forall(hint => all.exists(_.contains(hint))))
495+
@Test def `i16250a`: Unit =
496+
initially:
497+
run("import language.experimental.captureChecking")
498+
.andThen:
499+
assertEquals("", storedOutput().trim)
500+
// Verify capture checking syntax is accepted in subsequent inputs
501+
run("def foo[C^](x: AnyRef^{C}): AnyRef^{x} = x")
502+
assertEquals(
503+
"def foo[C >: caps.CapSet <: caps.CapSet^](x: AnyRef^{C}): AnyRef^{x}",
504+
storedOutput().trim)
503505

504-
@Test def `i16250b`: Unit = initially:
505-
val hints = List(
506-
"this language import is not allowed in the REPL",
507-
"To use this language feature, include the flag `-language:experimental.pureFunctions` when starting the REPL"
508-
)
509-
run("import language.experimental.pureFunctions")
510-
val all = lines()
511-
assertTrue(hints.forall(hint => all.exists(_.contains(hint))))
506+
@Test def `i16250b`: Unit =
507+
initially:
508+
run("import language.experimental.pureFunctions")
509+
.andThen:
510+
assertEquals("", storedOutput().trim)
511+
// Pure function arrow syntax requires pureFunctions
512+
run("val f: Int -> Int = (x: Int) => x + 1")
513+
assertTrue(storedOutput().trim.startsWith("val f: Int -> Int = Lambda$"))
514+
515+
@Test def `i16250c`: Unit =
516+
initially:
517+
run("import language.experimental.separationChecking")
518+
.andThen:
519+
assertEquals("", storedOutput().trim)
520+
// separationChecking implies captureChecking
521+
run("def foo[C^](x: AnyRef^{C}): AnyRef^{x} = x")
522+
assertEquals(
523+
"def foo[C >: caps.CapSet <: caps.CapSet^](x: AnyRef^{C}): AnyRef^{x}",
524+
storedOutput().trim)
525+
526+
@Test def `i16250d`: Unit =
527+
initially:
528+
run("import language.experimental.safe")
529+
.andThen:
530+
assertEquals("", storedOutput().trim)
531+
// safe implies captureChecking
532+
run("def foo[C^](x: AnyRef^{C}): AnyRef^{x} = x")
533+
assertEquals(
534+
"def foo[C >: caps.CapSet <: caps.CapSet^](x: AnyRef^{C}): AnyRef^{x}",
535+
storedOutput().trim)
536+
537+
@Test def `i16250 nested global language imports error`: Unit = initially:
538+
for feature <- List("captureChecking", "pureFunctions", "separationChecking", "safe") do
539+
run(s"def test = { import language.experimental.$feature; 1 }")
540+
assertTrue(s"expected toplevel error for $feature",
541+
storedOutput().contains("this language import is only allowed at the toplevel"))
512542

513543
@Test def `i22844 regression colon eol`: Unit = initially:
514544
run:

scaladoc/src/dotty/tools/scaladoc/snippets/WrappedSnippet.scala

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ object WrappedSnippet:
1010

1111
val indent: Int = 2
1212

13+
/** Matches import lines for global language features that must be at the toplevel. */
14+
private val globalLanguageImport =
15+
raw"import\s+language\s*\.\s*experimental\s*\.\s*(captureChecking|pureFunctions|separationChecking|safe)\b".r.unanchored
16+
17+
private def isGlobalLanguageImport(line: String): Boolean =
18+
globalLanguageImport.matches(line.trim)
19+
1320
def apply(
1421
str: String,
1522
packageName: Option[String],
@@ -19,12 +26,16 @@ object WrappedSnippet:
1926
val baos = new ByteArrayOutputStream()
2027
val ps = new PrintStream(baos)
2128

29+
val lines = str.split('\n')
30+
val (globalImports, rest) = lines.partition(isGlobalLanguageImport)
31+
2232
ps.startHide()
2333
ps.println(s"package ${packageName.getOrElse("snippets")}")
34+
globalImports.foreach(ps.println)
2435
ps.println("object Snippet {")
2536
ps.endHide()
2637

27-
str.split('\n').foreach(ps.printlnWithIndent(indent, _))
38+
rest.foreach(ps.printlnWithIndent(indent, _))
2839

2940
ps.startHide()
3041
ps.println("}")
@@ -34,7 +45,7 @@ object WrappedSnippet:
3445
baos.toString,
3546
outerLineOffset,
3647
outerColumnOffset,
37-
2 + 2 /*Hide tokens*/,
48+
2 + globalImports.length + 2 /*Hide tokens*/,
3849
indent
3950
)
4051

scaladoc/test/dotty/tools/scaladoc/snippets/SnippetCompilerTest.scala

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,25 @@ class SnippetCompilerTest {
6363
assertMessageLevelPresent(warningSnippet, MessageLevel.Warning)
6464
//No test for Info
6565
}
66+
67+
// i16250: global language imports should be hoisted outside the snippet wrapper
68+
@Test
69+
def snippetGlobalLanguageImports: Unit = {
70+
assertSuccessfulCompilation(
71+
"""|import language.experimental.pureFunctions
72+
|val x: Int -> Int = (a: Int) => a
73+
|""".stripMargin)
74+
assertSuccessfulCompilation(
75+
"""|import language.experimental.captureChecking
76+
|def foo[C^](x: AnyRef^{C}): AnyRef^{x} = x
77+
|""".stripMargin)
78+
assertSuccessfulCompilation(
79+
"""|import language.experimental.separationChecking
80+
|def foo[C^](x: AnyRef^{C}): AnyRef^{x} = x
81+
|""".stripMargin)
82+
assertSuccessfulCompilation(
83+
"""|import language.experimental.safe
84+
|def foo[C^](x: AnyRef^{C}): AnyRef^{x} = x
85+
|""".stripMargin)
86+
}
6687
}

tests/neg-custom-args/captures/language-imports.scala

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,3 @@ def test =
22
import language.experimental.captureChecking // error
33
import language.experimental.pureFunctions // error
44
1
5-
6-

0 commit comments

Comments
 (0)