Skip to content

Commit d7db737

Browse files
committed
Implement SIP-77: End Markers for Method Blocks
1 parent a9c731c commit d7db737

File tree

10 files changed

+358
-1
lines changed

10 files changed

+358
-1
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,13 @@ object Trees {
530530
*/
531531
def applyKind: ApplyKind =
532532
attachmentOrElse(untpd.KindOfApply, ApplyKind.Regular)
533+
534+
def setEndMarker(): this.type =
535+
putAttachment(untpd.HasEndMarker, ())
536+
this
537+
538+
def hasEndMarker: Boolean =
539+
hasAttachment(untpd.HasEndMarker)
533540
}
534541

535542
/** fun[args] */

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 endMarkersForMethodBlocks = experimental("endMarkersForMethodBlocks")
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+
(endMarkersForMethodBlocks, "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 enablesEndMarkersForMethodBlocks(using Context) = isAtLeast(`3.8`) || Feature.enabled(Feature.endMarkersForMethodBlocks)
5051

5152
def requiresNewSyntax = isAtLeast(future)
5253

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

Lines changed: 30 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.*
@@ -1543,6 +1544,7 @@ object Parsers {
15431544
def updateSpanOfLast(last: T): Unit =
15441545
last match
15451546
case last: WithEndMarker[t] => last.withEndMarker()
1547+
case apply: Apply if sourceVersion.enablesEndMarkersForMethodBlocks => apply.setEndMarker()
15461548
case _ =>
15471549
last.span = last.span.withEnd(in.lastCharOffset)
15481550

@@ -1562,6 +1564,17 @@ object Parsers {
15621564
case _: Match => in.token == MATCH
15631565
case _: New => in.token == NEW
15641566
case _: (ForYield | ForDo) => in.token == FOR
1567+
case apply: Apply if sourceVersion.enablesEndMarkersForMethodBlocks =>
1568+
// Extract method name from Apply node
1569+
val methodName = apply.attachmentOrElse(untpd.MethodName, null)
1570+
if methodName != null then
1571+
in.isIdent && in.name == methodName.toTermName
1572+
else
1573+
// Fallback to extracting from fun tree
1574+
apply.fun match
1575+
case Select(_, name) => in.isIdent && in.name == name.toTermName
1576+
case Ident(name) => in.isIdent && in.name == name.toTermName
1577+
case _ => false
15651578
case _ => false
15661579

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

29272957
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+
# End Markers for Method Blocks
2+
3+
End markers for method blocks 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.endMarkersForMethodBlocks
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.endMarkersForMethodBlocks
89+
```
90+
91+
Alternatively, you can enable it globally with the compiler flag:
92+
93+
```
94+
-language:experimental.endMarkersForMethodBlocks
95+
```
96+
97+
## When to Use
98+
99+
End markers for method blocks 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+
* [`endMarkersForMethodBlocks`](./end-markers-method-blocks.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/runtime/stdLibPatches/language.scala

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

179185
/** The deprecated object contains features that are no longer officially suypported in Scala.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import scala.language.experimental.endMarkersForMethodBlocks
2+
3+
object Test:
4+
def test(name: String)(body: => Unit): Unit =
5+
println(s"Running test: $name")
6+
body
7+
8+
def foo(arg: Int)(block: => Unit): Unit = block
9+
10+
def bar(x: Int): Unit = ()
11+
12+
def main(args: Array[String]): Unit =
13+
// Misaligned end marker example from SIP
14+
def testFunction = bar:
15+
//do something
16+
println("inside bar")
17+
end bar // error: misaligned end marker
18+
19+
val baz = foo(42):
20+
//do something
21+
end foo // error: misaligned end marker
22+
end baz
23+
24+
// Wrong method name examples
25+
test("my test"):
26+
val x = 1
27+
assert(x > 0)
28+
end wrong // error
29+
30+
foo(42):
31+
val result = 42 * 2
32+
println(s"Result: $result")
33+
end bar // error
34+
35+
// Wrong end marker for nested blocks
36+
test("nested test"):
37+
val x = 1
38+
val y = 2
39+
val z = x + y
40+
assert(z == 3)
41+
end foo // error
42+
43+
// Wrong end marker for complex test
44+
test("complex test"):
45+
val x = 1
46+
val y = 2
47+
val z = x + y
48+
assert(z == 3)
49+
println(s"Result: $z")
50+
end bar // error
51+
52+
// Wrong end marker for lambda
53+
val elements = List(1, 2, 3)
54+
elements.foreach:
55+
elem =>
56+
println(s"Element: $elem")
57+
end test // error
58+
59+
// Wrong end marker for explicit apply
60+
object Foo:
61+
def apply(block: => Unit): Unit = block
62+
63+
Foo.apply:
64+
println("Explicit apply call")
65+
end Foo // error
66+
67+
// Wrong end marker for implicit apply
68+
Foo:
69+
println("Implicit apply call")
70+
end apply // error
71+
72+
// Wrong end marker for class instance
73+
class Bar:
74+
def apply(block: => Unit): Unit = block
75+
76+
val bar = new Bar
77+
bar:
78+
println("Class instance apply call")
79+
end Bar // error
80+
81+
// Wrong end marker for curried method
82+
def curriedFoo(bar: String)(baz: String): Unit =
83+
println(s"$bar and $baz")
84+
85+
curriedFoo("abc"):
86+
"xyz"
87+
end curriedBar // error
88+
89+
// Wrong end marker for extension method
90+
extension (s: String)
91+
def withPrefix(prefix: String): String = s"$prefix$s"
92+
93+
"world".withPrefix:
94+
"hello"
95+
end withSuffix // error

0 commit comments

Comments
 (0)