From eeb5a25840cfeaf4263f385eb372368a35f9e595 Mon Sep 17 00:00:00 2001 From: Oron Date: Fri, 24 Oct 2025 22:21:15 +0300 Subject: [PATCH] Implement SIP-77: Method Block End Markers --- compiler/src/dotty/tools/dotc/ast/Trees.scala | 14 +- compiler/src/dotty/tools/dotc/ast/untpd.scala | 6 + .../src/dotty/tools/dotc/config/Feature.scala | 2 + .../tools/dotc/config/SourceVersion.scala | 1 + .../dotty/tools/dotc/parsing/Parsers.scala | 29 ++++ .../experimental/method-block-end-markers.md | 124 ++++++++++++++++++ docs/_docs/reference/experimental/overview.md | 3 +- library/src/scala/language.scala | 7 + .../runtime/stdLibPatches/language.scala | 7 + .../tools/pc/completions/Completions.scala | 16 ++- tests/neg/end-markers-method-blocks.scala | 95 ++++++++++++++ tests/pos/end-markers-method-blocks.scala | 85 ++++++++++++ 12 files changed, 385 insertions(+), 4 deletions(-) create mode 100644 docs/_docs/reference/experimental/method-block-end-markers.md create mode 100644 tests/neg/end-markers-method-blocks.scala create mode 100644 tests/pos/end-markers-method-blocks.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Trees.scala b/compiler/src/dotty/tools/dotc/ast/Trees.scala index ef64a025805a..818cb62bd2ee 100644 --- a/compiler/src/dotty/tools/dotc/ast/Trees.scala +++ b/compiler/src/dotty/tools/dotc/ast/Trees.scala @@ -319,7 +319,7 @@ object Trees { extension (mdef: untpd.DefTree) def mods: untpd.Modifiers = mdef.rawMods sealed trait WithEndMarker[+T <: Untyped]: - self: PackageDef[T] | NamedDefTree[T] => + self: PackageDef[T] | NamedDefTree[T] | Apply[T] => import WithEndMarker.* @@ -518,9 +518,19 @@ object Trees { /** fun(args) */ case class Apply[+T <: Untyped] private[ast] (fun: Tree[T], args: List[Tree[T]])(implicit @constructorOnly src: SourceFile) - extends GenericApply[T] { + extends GenericApply[T] with WithEndMarker[T] { type ThisTree[+T <: Untyped] = Apply[T] + protected def srcName(using Context): Name = + // Prefer stored method name when present (handles nested Apply cases) + this.attachmentOrElse(untpd.MethodName, null) match + case name: Name => name + case _ => + fun match + case Select(_, name) => name + case Ident(name) => name + case _ => nme.EMPTY + def setApplyKind(kind: ApplyKind) = putAttachment(untpd.KindOfApply, kind) this diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 17dbb5bff213..a9cf849c57c8 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -391,6 +391,12 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { val RetainsAnnot: Property.StickyKey[Unit] = Property.StickyKey() + /** Property key for marking Apply trees with end markers */ + val HasEndMarker: Property.StickyKey[Unit] = Property.StickyKey() + + /** Property key for storing method name in Apply trees for end marker matching */ + val MethodName: Property.StickyKey[Name] = Property.StickyKey() + // ------ Creation methods for untyped only ----------------- def Ident(name: Name)(implicit src: SourceFile): Ident = new Ident(name) diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 6eaa4d5c98a3..b4102cac7027 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -40,6 +40,7 @@ object Feature: val packageObjectValues = experimental("packageObjectValues") val multiSpreads = experimental("multiSpreads") val subCases = experimental("subCases") + val methodBlockEndMarkers = experimental("methodBlockEndMarkers") def experimentalAutoEnableFeatures(using Context): List[TermName] = defn.languageExperimentalFeatures @@ -69,6 +70,7 @@ object Feature: (into, "Allow into modifier on parameter types"), (modularity, "Enable experimental modularity features"), (packageObjectValues, "Enable experimental package objects as values"), + (methodBlockEndMarkers, "Enable experimental end markers for method blocks"), ) // legacy language features from Scala 2 that are no longer supported. diff --git a/compiler/src/dotty/tools/dotc/config/SourceVersion.scala b/compiler/src/dotty/tools/dotc/config/SourceVersion.scala index 59b958762042..80cdf1b65f30 100644 --- a/compiler/src/dotty/tools/dotc/config/SourceVersion.scala +++ b/compiler/src/dotty/tools/dotc/config/SourceVersion.scala @@ -47,6 +47,7 @@ enum SourceVersion: def enablesBetterFors(using Context) = isAtLeast(`3.8`) || (isAtLeast(`3.7`) && isPreviewEnabled) /** See PR #23441 and tests/neg/i23435-min */ def enablesDistributeAnd = !isAtLeast(`3.8`) + def enablesMethodBlockEndMarkers(using Context) = isAtLeast(`3.8`) || Feature.enabled(Feature.methodBlockEndMarkers) def requiresNewSyntax = isAtLeast(future) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 64eb442c239a..9d67a00fed81 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -19,6 +19,7 @@ import NameKinds.{WildcardParamName, QualifiedName} import NameOps.* import ast.{Positioned, Trees} import ast.Trees.* +import ast.untpd import StdNames.* import util.Spans.* import Constants.* @@ -1562,6 +1563,17 @@ object Parsers { case _: Match => in.token == MATCH case _: New => in.token == NEW case _: (ForYield | ForDo) => in.token == FOR + case apply: Apply if sourceVersion.enablesMethodBlockEndMarkers => + // Extract method name from Apply node + val methodName = apply.attachmentOrElse(untpd.MethodName, null) + if methodName != null then + in.isIdent && in.name == methodName.toTermName + else + // Fallback to extracting from fun tree + apply.fun match + case Select(_, name) => in.isIdent && in.name == name.toTermName + case Ident(name) => in.isIdent && in.name == name.toTermName + case _ => false case _ => false def endName = if in.token == IDENTIFIER then in.name.toString else tokenString(in.token) @@ -2922,6 +2934,23 @@ object Parsers { def mkApply(fn: Tree, args: (List[Tree], Boolean)): Tree = val res = Apply(fn, args._1) if args._2 then res.setApplyKind(ApplyKind.Using) + // Track method name for end marker support when using colon syntax + if sourceVersion.enablesMethodBlockEndMarkers then + val methodName = fn match + case Select(_, name) => name + case Ident(name) => name + case apply: Apply => + // For nested Apply (e.g., test("arg"):), extract the method name from the inner Apply + apply.attachmentOrElse(untpd.MethodName, null) match + case null => + apply.fun match + case Select(_, name) => name + case Ident(name) => name + case _ => return res + case name => name + case _ => return res + // Store method name as attachment for end marker matching + res.putAttachment(untpd.MethodName, methodName) res val argumentExpr: () => Tree = () => expr(Location.InArgs) match diff --git a/docs/_docs/reference/experimental/method-block-end-markers.md b/docs/_docs/reference/experimental/method-block-end-markers.md new file mode 100644 index 000000000000..92405693ce34 --- /dev/null +++ b/docs/_docs/reference/experimental/method-block-end-markers.md @@ -0,0 +1,124 @@ +# Method Block End Markers + +Method block end markers allow you to explicitly mark the end of method application blocks that use colon syntax (braceless arguments). + +## Syntax + +The syntax follows the pattern: + +```scala +methodName(args): + // block content +end methodName +``` + +## Examples + +### Simple Method Blocks + +```scala +import scala.language.experimental.methodBlockEndMarkers + +def test(name: String)(body: => Unit): Unit = + println(s"Running test: $name") + body + +test("my test"): + val x = 1 + assert(x > 0) +end test +``` + +### Nested Calls + +```scala +def foo(x: Int)(body: => Int): Int = body + +test("my test"): + foo(42): + val result = 42 * 2 + result + end foo +end test +``` + +### Apply Method Handling + +When dealing with `apply` methods, the end marker follows the explicit method name used in the call: + +**Explicit `apply` calls**: Use `end apply` when the method is called explicitly with `.apply`. + +```scala +object Foo: + def apply(block: => Unit): Unit = () + +Foo.apply: + // do something +end apply +``` + +**Implicit `apply` calls**: Use the name of the object/class instance that owns the `apply` method when it's called implicitly. + +```scala +object Foo: + def apply(block: => Unit): Unit = () + +Foo: + // do something +end Foo +``` + +```scala +class Foo: + def apply(block: => Unit): Unit = () + +val foo = new Foo +foo: + // do something +end foo +``` + +This rule ensures that the end marker always corresponds to the syntactically visible method name, making the code self-documenting and consistent with the principle that end markers should match the surface syntax. + +## How to Enable + +To use end markers for method blocks, you need to enable the experimental feature: + +```scala +import scala.language.experimental.methodBlockEndMarkers +``` + +Alternatively, you can enable it globally with the compiler flag: + +``` +-language:experimental.methodBlockEndMarkers +``` + +## When to Use + +Method block end markers are particularly useful when: + +- You have deeply nested method calls with colon syntax +- You want to improve code readability by explicitly marking block boundaries +- You're working with DSLs or testing frameworks that use method blocks extensively + +## Limitations + +- End markers only work with method applications that use colon syntax (braceless arguments) +- The end marker name must exactly match the method name +- This feature is experimental and may undergo API changes in future releases + +## Error Cases + +The compiler will report errors for misaligned end markers: + +```scala +test("my test"): + val x = 1 + assert(x > 0) +end wrong // Error: misaligned end marker +``` + +## Interaction with Other Features + +This feature works alongside the existing `fewerBraces` feature and follows the same syntactic patterns. It extends the end marker functionality to method application blocks. diff --git a/docs/_docs/reference/experimental/overview.md b/docs/_docs/reference/experimental/overview.md index 9242056d8405..638a03cb5691 100644 --- a/docs/_docs/reference/experimental/overview.md +++ b/docs/_docs/reference/experimental/overview.md @@ -11,6 +11,7 @@ All experimental language features can be found under the `scala.language.experi They are enabled by importing the feature or using the `-language` compiler flag. * [`erasedDefinitions`](./erased-defs.md): Enable support for `erased` modifier. +* [`methodBlockEndMarkers`](./method-block-end-markers.md): Enable support for end markers for method blocks. * `fewerBraces`: Enable support for using indentation for arguments. * [`genericNumberLiterals`](./numeric-literals.md): Enable support for generic number literals. * [`namedTypeArguments`](./named-typeargs.md): Enable support for named type arguments @@ -32,4 +33,4 @@ Hence, dependent projects also have to be experimental. Some experimental language features that are still in research and development can be enabled with special compiler options. These include * [`-Yexplicit-nulls`](./explicit-nulls.md). Enable support for tracking null references in the type system. -* [`-Ycc`](./capture-checking/cc.md). Enable support for capture checking. +* [`-Ycc`](./cc.md). Enable support for capture checking. diff --git a/library/src/scala/language.scala b/library/src/scala/language.scala index ffc9ad2fdca1..0c95e60a1075 100644 --- a/library/src/scala/language.scala +++ b/library/src/scala/language.scala @@ -367,6 +367,13 @@ object language { */ @compileTimeOnly("`subCases` can only be used at compile time in import statements") object subCases + + /** Experimental support for end markers for method blocks. + * + * @see [[https://github.com/scala/improvement-proposals/pull/77]] + */ + @compileTimeOnly("`methodBlockEndMarkers` can only be used at compile time in import statements") + object methodBlockEndMarkers } /** The deprecated object contains features that are no longer officially suypported in Scala. diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index abd2a34efa77..d8c49e0c5a5c 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -174,6 +174,13 @@ object language: */ @compileTimeOnly("`subCases` can only be used at compile time in import statements") object subCases + + /** Experimental support for end markers for method blocks. + * + * @see [[https://github.com/scala/improvement-proposals/pull/77]] + */ + @compileTimeOnly("`methodBlockEndMarkers` can only be used at compile time in import statements") + object methodBlockEndMarkers end experimental /** The deprecated object contains features that are no longer officially suypported in Scala. diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala index 72b65b3cadb9..ea4d2e8a44e3 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala @@ -108,7 +108,14 @@ class Completions( case _ => false else false - if generalExclude then false + def isLanguageExperimental: Boolean = + val n = sym.fullName.show.replace("$","") + n.contains(".language.experimental.") || n.endsWith(".language.experimental") + + val excludeLanguageExperimental = + !completionMode.is(Mode.ImportOrExport) && isLanguageExperimental + + if generalExclude || excludeLanguageExperimental then false else if completionMode.is(Mode.Type) then true else !isWildcardParam(sym) && (sym.isTerm || sym.is(Package)) end if @@ -585,6 +592,13 @@ class Completions( if completionMode.is(Mode.Scope) && query.nonEmpty then val visitor = new CompilerSearchVisitor(sym => if Completion.isValidCompletionSymbol(sym, completionMode, isNew) && + // Avoid suggesting experimental language import symbols (`scala.language.experimental.*`) + // as auto-import completions in regular identifier positions. + // They are still available in import positions. + { + val n = sym.fullName.show + !n.contains(".language.experimental.") && !n.endsWith(".language.experimental") + } && !(sym.is(Flags.ExtensionMethod) || (sym.maybeOwner.is(Flags.Implicit) && sym.maybeOwner.isClass)) then indexedContext.lookupSym(sym) match diff --git a/tests/neg/end-markers-method-blocks.scala b/tests/neg/end-markers-method-blocks.scala new file mode 100644 index 000000000000..048ed1c6fc7f --- /dev/null +++ b/tests/neg/end-markers-method-blocks.scala @@ -0,0 +1,95 @@ +import scala.language.experimental.methodBlockEndMarkers + +object Test: + def test(name: String)(body: => Unit): Unit = + println(s"Running test: $name") + body + + def foo(arg: Int)(block: => Unit): Unit = block + + def bar(x: Int): Unit = () + + def main(args: Array[String]): Unit = + // Misaligned end marker example from SIP + def testFunction = bar: + //do something + println("inside bar") + end bar // error: misaligned end marker + + val baz = foo(42): + //do something + end foo // error: misaligned end marker + end baz + + // Wrong method name examples + test("my test"): + val x = 1 + assert(x > 0) + end wrong // error + + foo(42): + val result = 42 * 2 + println(s"Result: $result") + end bar // error + + // Wrong end marker for nested blocks + test("nested test"): + val x = 1 + val y = 2 + val z = x + y + assert(z == 3) + end foo // error + + // Wrong end marker for complex test + test("complex test"): + val x = 1 + val y = 2 + val z = x + y + assert(z == 3) + println(s"Result: $z") + end bar // error + + // Wrong end marker for lambda + val elements = List(1, 2, 3) + elements.foreach: + elem => + println(s"Element: $elem") + end test // error + + // Wrong end marker for explicit apply + object Foo: + def apply(block: => Unit): Unit = block + + Foo.apply: + println("Explicit apply call") + end Foo // error + + // Wrong end marker for implicit apply + Foo: + println("Implicit apply call") + end apply // error + + // Wrong end marker for class instance + class Bar: + def apply(block: => Unit): Unit = block + + val bar = new Bar + bar: + println("Class instance apply call") + end Bar // error + + // Wrong end marker for curried method + def curriedFoo(bar: String)(baz: String): Unit = + println(s"$bar and $baz") + + curriedFoo("abc"): + "xyz" + end curriedBar // error + + // Wrong end marker for extension method + extension (s: String) + def withPrefix(prefix: String): String = s"$prefix$s" + + "world".withPrefix: + "hello" + end withSuffix // error diff --git a/tests/pos/end-markers-method-blocks.scala b/tests/pos/end-markers-method-blocks.scala new file mode 100644 index 000000000000..1e763951d9a5 --- /dev/null +++ b/tests/pos/end-markers-method-blocks.scala @@ -0,0 +1,85 @@ +import scala.language.experimental.methodBlockEndMarkers + +object Test: + def test(name: String)(body: => Unit): Unit = + println(s"Running test: $name") + body + + def foo(arg: Int)(block: => Unit): Unit = block + + def main(args: Array[String]): Unit = + // Basic examples from SIP + test("my test"): + val x = 1 + assert(x > 0) + end test + + foo(42): + val result = 42 * 2 + println(s"Result: $result") + end foo + + // Nested blocks example from SIP + test("very long test"): + locally: + val setup = "setup" + println(setup) + end locally + // assertions... + assert(true) + end test + + // Lambda with colon syntax example from SIP + val elements = List(1, 2, 3) + elements.foreach: + elem => + println(s"Element: $elem") + end foreach + + // Explicit apply method example from SIP + object Foo: + def apply(block: => Unit): Unit = block + + Foo.apply: + println("Explicit apply call") + end apply + + // Implicit apply method example from SIP + Foo: + println("Implicit apply call") + end Foo + + // Class with apply method example from SIP + class Bar: + def apply(block: => Unit): Unit = block + + val bar = new Bar + bar: + println("Class instance apply call") + end bar + + // Curried method example from SIP + def curriedFoo(bar: String)(baz: String): Unit = + println(s"$bar and $baz") + + curriedFoo("abc"): + "xyz" + end curriedFoo + + // Curried class example from SIP + class CurriedFoo(bar: String): + def apply(baz: String): Unit = + println(s"$bar and $baz") + + def createCurriedFoo(bar: String) = CurriedFoo(bar) + createCurriedFoo("abc"): + "xyz" + end createCurriedFoo + + // Extension method example + extension (s: String) + def withPrefix(prefix: String): String = s"$prefix$s" + + "world".withPrefix: + "hello" + end withPrefix