Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions compiler/src/dotty/tools/dotc/ast/Trees.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.*

Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/untpd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/config/Feature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/config/SourceVersion.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
29 changes: 29 additions & 0 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
124 changes: 124 additions & 0 deletions docs/_docs/reference/experimental/method-block-end-markers.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion docs/_docs/reference/experimental/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
7 changes: 7 additions & 0 deletions library/src/scala/language.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions library/src/scala/runtime/stdLibPatches/language.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading