Skip to content

Commit eeb5a25

Browse files
committed
Implement SIP-77: Method Block End Markers
1 parent a9c731c commit eeb5a25

File tree

12 files changed

+385
-4
lines changed

12 files changed

+385
-4
lines changed

compiler/src/dotty/tools/dotc/ast/Trees.scala

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ object Trees {
319319
extension (mdef: untpd.DefTree) def mods: untpd.Modifiers = mdef.rawMods
320320

321321
sealed trait WithEndMarker[+T <: Untyped]:
322-
self: PackageDef[T] | NamedDefTree[T] =>
322+
self: PackageDef[T] | NamedDefTree[T] | Apply[T] =>
323323

324324
import WithEndMarker.*
325325

@@ -518,9 +518,19 @@ object Trees {
518518

519519
/** fun(args) */
520520
case class Apply[+T <: Untyped] private[ast] (fun: Tree[T], args: List[Tree[T]])(implicit @constructorOnly src: SourceFile)
521-
extends GenericApply[T] {
521+
extends GenericApply[T] with WithEndMarker[T] {
522522
type ThisTree[+T <: Untyped] = Apply[T]
523523

524+
protected def srcName(using Context): Name =
525+
// Prefer stored method name when present (handles nested Apply cases)
526+
this.attachmentOrElse(untpd.MethodName, null) match
527+
case name: Name => name
528+
case _ =>
529+
fun match
530+
case Select(_, name) => name
531+
case Ident(name) => name
532+
case _ => nme.EMPTY
533+
524534
def setApplyKind(kind: ApplyKind) =
525535
putAttachment(untpd.KindOfApply, kind)
526536
this

compiler/src/dotty/tools/dotc/ast/untpd.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,12 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
391391

392392
val RetainsAnnot: Property.StickyKey[Unit] = Property.StickyKey()
393393

394+
/** Property key for marking Apply trees with end markers */
395+
val HasEndMarker: Property.StickyKey[Unit] = Property.StickyKey()
396+
397+
/** Property key for storing method name in Apply trees for end marker matching */
398+
val MethodName: Property.StickyKey[Name] = Property.StickyKey()
399+
394400
// ------ Creation methods for untyped only -----------------
395401

396402
def Ident(name: Name)(implicit src: SourceFile): Ident = new Ident(name)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ object Feature:
4040
val packageObjectValues = experimental("packageObjectValues")
4141
val multiSpreads = experimental("multiSpreads")
4242
val subCases = experimental("subCases")
43+
val methodBlockEndMarkers = experimental("methodBlockEndMarkers")
4344

4445
def experimentalAutoEnableFeatures(using Context): List[TermName] =
4546
defn.languageExperimentalFeatures
@@ -69,6 +70,7 @@ object Feature:
6970
(into, "Allow into modifier on parameter types"),
7071
(modularity, "Enable experimental modularity features"),
7172
(packageObjectValues, "Enable experimental package objects as values"),
73+
(methodBlockEndMarkers, "Enable experimental end markers for method blocks"),
7274
)
7375

7476
// legacy language features from Scala 2 that are no longer supported.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ enum SourceVersion:
4747
def enablesBetterFors(using Context) = isAtLeast(`3.8`) || (isAtLeast(`3.7`) && isPreviewEnabled)
4848
/** See PR #23441 and tests/neg/i23435-min */
4949
def enablesDistributeAnd = !isAtLeast(`3.8`)
50+
def enablesMethodBlockEndMarkers(using Context) = isAtLeast(`3.8`) || Feature.enabled(Feature.methodBlockEndMarkers)
5051

5152
def requiresNewSyntax = isAtLeast(future)
5253

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import NameKinds.{WildcardParamName, QualifiedName}
1919
import NameOps.*
2020
import ast.{Positioned, Trees}
2121
import ast.Trees.*
22+
import ast.untpd
2223
import StdNames.*
2324
import util.Spans.*
2425
import Constants.*
@@ -1562,6 +1563,17 @@ object Parsers {
15621563
case _: Match => in.token == MATCH
15631564
case _: New => in.token == NEW
15641565
case _: (ForYield | ForDo) => in.token == FOR
1566+
case apply: Apply if sourceVersion.enablesMethodBlockEndMarkers =>
1567+
// Extract method name from Apply node
1568+
val methodName = apply.attachmentOrElse(untpd.MethodName, null)
1569+
if methodName != null then
1570+
in.isIdent && in.name == methodName.toTermName
1571+
else
1572+
// Fallback to extracting from fun tree
1573+
apply.fun match
1574+
case Select(_, name) => in.isIdent && in.name == name.toTermName
1575+
case Ident(name) => in.isIdent && in.name == name.toTermName
1576+
case _ => false
15651577
case _ => false
15661578

15671579
def endName = if in.token == IDENTIFIER then in.name.toString else tokenString(in.token)
@@ -2922,6 +2934,23 @@ object Parsers {
29222934
def mkApply(fn: Tree, args: (List[Tree], Boolean)): Tree =
29232935
val res = Apply(fn, args._1)
29242936
if args._2 then res.setApplyKind(ApplyKind.Using)
2937+
// Track method name for end marker support when using colon syntax
2938+
if sourceVersion.enablesMethodBlockEndMarkers then
2939+
val methodName = fn match
2940+
case Select(_, name) => name
2941+
case Ident(name) => name
2942+
case apply: Apply =>
2943+
// For nested Apply (e.g., test("arg"):), extract the method name from the inner Apply
2944+
apply.attachmentOrElse(untpd.MethodName, null) match
2945+
case null =>
2946+
apply.fun match
2947+
case Select(_, name) => name
2948+
case Ident(name) => name
2949+
case _ => return res
2950+
case name => name
2951+
case _ => return res
2952+
// Store method name as attachment for end marker matching
2953+
res.putAttachment(untpd.MethodName, methodName)
29252954
res
29262955

29272956
val argumentExpr: () => Tree = () => expr(Location.InArgs) match
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Method Block End Markers
2+
3+
Method block end markers allow you to explicitly mark the end of method application blocks that use colon syntax (braceless arguments).
4+
5+
## Syntax
6+
7+
The syntax follows the pattern:
8+
9+
```scala
10+
methodName(args):
11+
// block content
12+
end methodName
13+
```
14+
15+
## Examples
16+
17+
### Simple Method Blocks
18+
19+
```scala
20+
import scala.language.experimental.methodBlockEndMarkers
21+
22+
def test(name: String)(body: => Unit): Unit =
23+
println(s"Running test: $name")
24+
body
25+
26+
test("my test"):
27+
val x = 1
28+
assert(x > 0)
29+
end test
30+
```
31+
32+
### Nested Calls
33+
34+
```scala
35+
def foo(x: Int)(body: => Int): Int = body
36+
37+
test("my test"):
38+
foo(42):
39+
val result = 42 * 2
40+
result
41+
end foo
42+
end test
43+
```
44+
45+
### Apply Method Handling
46+
47+
When dealing with `apply` methods, the end marker follows the explicit method name used in the call:
48+
49+
**Explicit `apply` calls**: Use `end apply` when the method is called explicitly with `.apply`.
50+
51+
```scala
52+
object Foo:
53+
def apply(block: => Unit): Unit = ()
54+
55+
Foo.apply:
56+
// do something
57+
end apply
58+
```
59+
60+
**Implicit `apply` calls**: Use the name of the object/class instance that owns the `apply` method when it's called implicitly.
61+
62+
```scala
63+
object Foo:
64+
def apply(block: => Unit): Unit = ()
65+
66+
Foo:
67+
// do something
68+
end Foo
69+
```
70+
71+
```scala
72+
class Foo:
73+
def apply(block: => Unit): Unit = ()
74+
75+
val foo = new Foo
76+
foo:
77+
// do something
78+
end foo
79+
```
80+
81+
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.
82+
83+
## How to Enable
84+
85+
To use end markers for method blocks, you need to enable the experimental feature:
86+
87+
```scala
88+
import scala.language.experimental.methodBlockEndMarkers
89+
```
90+
91+
Alternatively, you can enable it globally with the compiler flag:
92+
93+
```
94+
-language:experimental.methodBlockEndMarkers
95+
```
96+
97+
## When to Use
98+
99+
Method block end markers are particularly useful when:
100+
101+
- You have deeply nested method calls with colon syntax
102+
- You want to improve code readability by explicitly marking block boundaries
103+
- You're working with DSLs or testing frameworks that use method blocks extensively
104+
105+
## Limitations
106+
107+
- End markers only work with method applications that use colon syntax (braceless arguments)
108+
- The end marker name must exactly match the method name
109+
- This feature is experimental and may undergo API changes in future releases
110+
111+
## Error Cases
112+
113+
The compiler will report errors for misaligned end markers:
114+
115+
```scala
116+
test("my test"):
117+
val x = 1
118+
assert(x > 0)
119+
end wrong // Error: misaligned end marker
120+
```
121+
122+
## Interaction with Other Features
123+
124+
This feature works alongside the existing `fewerBraces` feature and follows the same syntactic patterns. It extends the end marker functionality to method application blocks.

docs/_docs/reference/experimental/overview.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ All experimental language features can be found under the `scala.language.experi
1111
They are enabled by importing the feature or using the `-language` compiler flag.
1212

1313
* [`erasedDefinitions`](./erased-defs.md): Enable support for `erased` modifier.
14+
* [`methodBlockEndMarkers`](./method-block-end-markers.md): Enable support for end markers for method blocks.
1415
* `fewerBraces`: Enable support for using indentation for arguments.
1516
* [`genericNumberLiterals`](./numeric-literals.md): Enable support for generic number literals.
1617
* [`namedTypeArguments`](./named-typeargs.md): Enable support for named type arguments
@@ -32,4 +33,4 @@ Hence, dependent projects also have to be experimental.
3233
Some experimental language features that are still in research and development can be enabled with special compiler options. These include
3334

3435
* [`-Yexplicit-nulls`](./explicit-nulls.md). Enable support for tracking null references in the type system.
35-
* [`-Ycc`](./capture-checking/cc.md). Enable support for capture checking.
36+
* [`-Ycc`](./cc.md). Enable support for capture checking.

library/src/scala/language.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,13 @@ object language {
367367
*/
368368
@compileTimeOnly("`subCases` can only be used at compile time in import statements")
369369
object subCases
370+
371+
/** Experimental support for end markers for method blocks.
372+
*
373+
* @see [[https://github.com/scala/improvement-proposals/pull/77]]
374+
*/
375+
@compileTimeOnly("`methodBlockEndMarkers` can only be used at compile time in import statements")
376+
object methodBlockEndMarkers
370377
}
371378

372379
/** The deprecated object contains features that are no longer officially suypported in Scala.

library/src/scala/runtime/stdLibPatches/language.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,13 @@ object language:
174174
*/
175175
@compileTimeOnly("`subCases` can only be used at compile time in import statements")
176176
object subCases
177+
178+
/** Experimental support for end markers for method blocks.
179+
*
180+
* @see [[https://github.com/scala/improvement-proposals/pull/77]]
181+
*/
182+
@compileTimeOnly("`methodBlockEndMarkers` can only be used at compile time in import statements")
183+
object methodBlockEndMarkers
177184
end experimental
178185

179186
/** The deprecated object contains features that are no longer officially suypported in Scala.

presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,14 @@ class Completions(
108108
case _ => false
109109
else false
110110

111-
if generalExclude then false
111+
def isLanguageExperimental: Boolean =
112+
val n = sym.fullName.show.replace("$","")
113+
n.contains(".language.experimental.") || n.endsWith(".language.experimental")
114+
115+
val excludeLanguageExperimental =
116+
!completionMode.is(Mode.ImportOrExport) && isLanguageExperimental
117+
118+
if generalExclude || excludeLanguageExperimental then false
112119
else if completionMode.is(Mode.Type) then true
113120
else !isWildcardParam(sym) && (sym.isTerm || sym.is(Package))
114121
end if
@@ -585,6 +592,13 @@ class Completions(
585592
if completionMode.is(Mode.Scope) && query.nonEmpty then
586593
val visitor = new CompilerSearchVisitor(sym =>
587594
if Completion.isValidCompletionSymbol(sym, completionMode, isNew) &&
595+
// Avoid suggesting experimental language import symbols (`scala.language.experimental.*`)
596+
// as auto-import completions in regular identifier positions.
597+
// They are still available in import positions.
598+
{
599+
val n = sym.fullName.show
600+
!n.contains(".language.experimental.") && !n.endsWith(".language.experimental")
601+
} &&
588602
!(sym.is(Flags.ExtensionMethod) || (sym.maybeOwner.is(Flags.Implicit) && sym.maybeOwner.isClass))
589603
then
590604
indexedContext.lookupSym(sym) match

0 commit comments

Comments
 (0)