Skip to content
Draft
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
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/config/Feature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ object Feature:
val quotedPatternsWithPolymorphicFunctions = experimental("quotedPatternsWithPolymorphicFunctions")
val packageObjectValues = experimental("packageObjectValues")
val subCases = experimental("subCases")
val relaxedLambdaSyntax = experimental("relaxedLambdaSyntax")

def experimentalAutoEnableFeatures(using Context): List[TermName] =
defn.languageExperimentalFeatures
Expand Down
114 changes: 78 additions & 36 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1085,27 +1085,51 @@ object Parsers {
}

/** Is the token sequence following the current `:` token classified as a lambda?
* This is the case if the input starts with an identifier, a wildcard, or
* something enclosed in (...) or [...], and this is followed by a `=>` or `?=>`
* and an INDENT.
*/
def followingIsLambdaAfterColon(): Boolean =
* If yes return a defined parsing function to parse the lambda body, if not
* return None. The case is triggered in two situations:
* 1. If the input starts with an identifier, a wildcard, or something
* enclosed in (...) or [...], this is followed by a `=>` or `?=>`,
* and one of the following two subcases applies:
* 1a. The next token is an indent. In this case the return parsing function parses
* an Expr in location Location.InColonArg.
* 1b. Under relaxedLambdaSyntax: the next token is on the same line and the enclosing region is not `(...)`.
* In this case the parsing function parses an Expr in location Location.InColonArg
* enclosed in a SingleLineLambda region, and then eats the ENDlambda token
* generated by the Scanner at the end of that region.
* The reason for excluding (1b) in regions enclosed in parentheses is to avoid
* an ambiguity with type ascription `(x: A => B)`, where function types are only
* allowed inside parentheses.
* 2. Under relaxedLambdaSyntax: the input starts with a `case`.
*/
def followingIsLambdaAfterColon(): Option[() => Tree] =
val lookahead = in.LookaheadScanner(allowIndent = true)
.tap(_.currentRegion.knownWidth = in.currentRegion.indentWidth)
def isArrowIndent() =
lookahead.isArrow
&& {
def isArrowIndent(): Option[() => Tree] =
if lookahead.isArrow then
lookahead.observeArrowIndented()
lookahead.token == INDENT || lookahead.token == EOF
}
if lookahead.token == INDENT || lookahead.token == EOF then
Some(() => expr(Location.InColonArg))
else if in.featureEnabled(Feature.relaxedLambdaSyntax)
&& !in.currentRegion.isInstanceOf[InParens]
then
Some: () =>
val t = inSepRegion(SingleLineLambda(_)):
expr(Location.InColonArg)
accept(ENDlambda)
t
else None
else None
lookahead.nextToken()
if lookahead.isIdent || lookahead.token == USCORE then
lookahead.nextToken()
isArrowIndent()
else if lookahead.token == LPAREN || lookahead.token == LBRACKET then
lookahead.skipParens()
isArrowIndent()
else false
else if lookahead.token == CASE && in.featureEnabled(Feature.relaxedLambdaSyntax) then
Some(() => singleCaseMatch())
else
None

/** Can the next lookahead token start an operand as defined by
* leadingOperandTokens, or is postfix ops enabled?
Expand Down Expand Up @@ -1170,12 +1194,19 @@ object Parsers {
case _ => infixOp
}

/** True if we are seeing a lambda argument after a colon of the form:
/** Optionally, if we are seeing a lambda argument after a colon of the form
* : (params) =>
* body
* or a single-line lambda (under relaxedLambdaSyntax)
* : (params) => body
* or a case clause (under relaxedLambdaSyntax)
* : case pat guard => rhs
* then return the function used to parse `body` or the case clause.
*/
def isColonLambda =
sourceVersion.enablesFewerBraces && in.token == COLONfollow && followingIsLambdaAfterColon()
def detectColonLambda: Option[() => Tree] =
if sourceVersion.enablesFewerBraces && in.token == COLONfollow
then followingIsLambdaAfterColon()
else None

/** operand { infixop operand | MatchClause } [postfixop],
*
Expand All @@ -1199,17 +1230,19 @@ object Parsers {
opStack = OpInfo(top1, op, in.offset) :: opStack
colonAtEOLOpt()
newLineOptWhenFollowing(canStartOperand)
if isColonLambda then
in.nextToken()
recur(expr(Location.InColonArg))
else if maybePostfix && !canStartOperand(in.token) then
val topInfo = opStack.head
opStack = opStack.tail
val od = reduceStack(base, topInfo.operand, 0, true, in.name, isType)
atSpan(startOffset(od), topInfo.offset) {
PostfixOp(od, topInfo.operator)
}
else recur(operand(location))
detectColonLambda match
case Some(parseExpr) =>
in.nextToken()
recur(parseExpr())
case _ =>
if maybePostfix && !canStartOperand(in.token) then
val topInfo = opStack.head
opStack = opStack.tail
val od = reduceStack(base, topInfo.operand, 0, true, in.name, isType)
atSpan(startOffset(od), topInfo.offset) {
PostfixOp(od, topInfo.operator)
}
else recur(operand(location))
else
val t = reduceStack(base, top, minPrec, leftAssoc = true, in.name, isType)
if !isType && in.token == MATCH then recurAtMinPrec(matchClause(t))
Expand Down Expand Up @@ -2353,6 +2386,7 @@ object Parsers {

/** Expr ::= [`implicit'] FunParams (‘=>’ | ‘?=>’) Expr
* | TypTypeParamClause ‘=>’ Expr
* | ExprCaseClause -- under experimental.relaxedLambdaSyntax
* | Expr1
* FunParams ::= Bindings
* | id
Expand Down Expand Up @@ -2404,6 +2438,8 @@ object Parsers {
val arrowOffset = accept(ARROW)
val body = expr(location)
makePolyFunction(tparams, body, "literal", errorTermTree(arrowOffset), start, arrowOffset)
case CASE if in.featureEnabled(Feature.relaxedLambdaSyntax) =>
singleCaseMatch()
case _ =>
val saved = placeholderParams
placeholderParams = Nil
Expand Down Expand Up @@ -2467,9 +2503,8 @@ object Parsers {
if in.token == CATCH then
val span = in.offset
in.nextToken()
(if in.token == CASE then Match(EmptyTree, caseClause(exprOnly = true) :: Nil)
else subExpr(),
span)
(if in.token == CASE then singleCaseMatch() else subExpr(),
span)
else (EmptyTree, -1)

handler match {
Expand Down Expand Up @@ -2766,6 +2801,8 @@ object Parsers {
* | SimpleExpr1 ColonArgument
* ColonArgument ::= colon [LambdaStart]
* indent (CaseClauses | Block) outdent
* | colon LambdaStart expr ENDlambda -- under experimental.relaxedLambdaSyntax
* | colon ExprCaseClause -- under experimental.relaxedLambdaSyntax
* LambdaStart ::= FunParams (‘=>’ | ‘?=>’)
* | TypTypeParamClause ‘=>’
* ColonArgBody ::= indent (CaseClauses | Block) outdent
Expand Down Expand Up @@ -2848,12 +2885,14 @@ object Parsers {
makeParameter(name.asTermName, typedOpt(), Modifiers(), isBackquoted = isBackquoted(id))
}
case _ => t
else if isColonLambda then
val app = atSpan(startOffset(t), in.skipToken()) {
Apply(t, expr(Location.InColonArg) :: Nil)
}
simpleExprRest(app, location, canApply = true)
else t
else detectColonLambda match
case Some(parseExpr) =>
val app =
atSpan(startOffset(t), in.skipToken()):
Apply(t, parseExpr() :: Nil)
simpleExprRest(app, location, canApply = true)
case None =>
t
end simpleExprRest

/** SimpleExpr ::= ‘new’ ConstrApp {`with` ConstrApp} [TemplateBody]
Expand Down Expand Up @@ -3160,9 +3199,9 @@ object Parsers {
case ARROW => atSpan(in.skipToken()):
if exprOnly then
if in.indentSyntax && in.isAfterLineEnd && in.token != INDENT then
warning(em"""Misleading indentation: this expression forms part of the preceding catch case.
warning(em"""Misleading indentation: this expression forms part of the preceding case.
|If this is intended, it should be indented for clarity.
|Otherwise, if the handler is intended to be empty, use a multi-line catch with
|Otherwise, if the handler is intended to be empty, use a multi-line match or catch with
|an indented case.""")
expr()
else block()
Expand All @@ -3178,6 +3217,9 @@ object Parsers {
CaseDef(pat, grd1, body)
}

def singleCaseMatch() =
Match(EmptyTree, caseClause(exprOnly = true) :: Nil)

/** TypeCaseClause ::= ‘case’ (InfixType | ‘_’) ‘=>’ Type [semi]
*/
def typeCaseClause(): CaseDef = atSpan(in.offset) {
Expand Down
8 changes: 7 additions & 1 deletion compiler/src/dotty/tools/dotc/parsing/Scanners.scala
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,9 @@ object Scanners {
&& !statCtdTokens.contains(lastToken)
&& !isTrailingBlankLine

if newlineIsSeparating
if currentRegion.closedBy == ENDlambda then
insert(ENDlambda, lineOffset)
else if newlineIsSeparating
&& canEndStatTokens.contains(lastToken)
&& canStartStatTokens.contains(token)
&& !isLeadingInfixOperator(nextWidth)
Expand Down Expand Up @@ -1599,6 +1601,8 @@ object Scanners {
* InParens a pair of parentheses (...) or brackets [...]
* InBraces a pair of braces { ... }
* Indented a pair of <indent> ... <outdent> tokens
* InCase a case of a match
* SingleLineLambda the rest of a line following a `:`
*/
abstract class Region(val closedBy: Token):

Expand Down Expand Up @@ -1667,6 +1671,7 @@ object Scanners {
case _: InBraces => "}"
case _: InCase => "=>"
case _: Indented => "UNDENT"
case _: SingleLineLambda => "end of single-line lambda"

/** Show open regions as list of lines with decreasing indentations */
def visualize: String =
Expand All @@ -1680,6 +1685,7 @@ object Scanners {
case class InParens(prefix: Token, outer: Region) extends Region(prefix + 1)
case class InBraces(outer: Region) extends Region(RBRACE)
case class InCase(outer: Region) extends Region(OUTDENT)
case class SingleLineLambda(outer: Region) extends Region(ENDlambda)

/** A class describing an indentation region.
* @param width The principal indentation width
Expand Down
6 changes: 4 additions & 2 deletions compiler/src/dotty/tools/dotc/parsing/Tokens.scala
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,10 @@ object Tokens extends TokensCommon {
// A `:` recognized as starting an indentation block
inline val SELFARROW = 90; enter(SELFARROW, "=>") // reclassified ARROW following self-type

inline val ENDlambda = 99; enter(ENDlambda, "end of single-line lambda")

/** XML mode */
inline val XMLSTART = 99; enter(XMLSTART, "$XMLSTART$<") // TODO: deprecate
inline val XMLSTART = 100; enter(XMLSTART, "$XMLSTART$<") // TODO: deprecate

final val alphaKeywords: TokenSet = tokenRange(IF, END)
final val symbolicKeywords: TokenSet = tokenRange(USCORE, CTXARROW)
Expand Down Expand Up @@ -267,7 +269,7 @@ object Tokens extends TokensCommon {
final val canStartStatTokens3: TokenSet = canStartExprTokens3 | mustStartStatTokens | BitSet(
AT, CASE, END)

final val canEndStatTokens: TokenSet = atomicExprTokens | BitSet(TYPE, GIVEN, RPAREN, RBRACE, RBRACKET, OUTDENT)
final val canEndStatTokens: TokenSet = atomicExprTokens | BitSet(TYPE, GIVEN, RPAREN, RBRACE, RBRACKET, OUTDENT, ENDlambda)

/** Tokens that stop a lookahead scan search for a `<-`, `then`, or `do`.
* Used for disambiguating between old and new syntax.
Expand Down
4 changes: 4 additions & 0 deletions docs/_docs/internals/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ CapFilter ::= ‘.’ ‘as’ ‘[’ QualId ’]’
```ebnf
Expr ::= FunParams (‘=>’ | ‘?=>’) Expr Function(args, expr), Function(ValDef([implicit], id, TypeTree(), EmptyTree), expr)
| TypTypeParamClause ‘=>’ Expr PolyFunction(ts, expr)
| ExprCaseClause
| Expr1
BlockResult ::= FunParams (‘=>’ | ‘?=>’) Block
| TypTypeParamClause ‘=>’ Block
Expand Down Expand Up @@ -295,6 +296,9 @@ SimpleExpr ::= SimpleRef
| XmlExpr -- to be dropped
ColonArgument ::= colon [LambdaStart]
indent (CaseClauses | Block) outdent
| colon LambdaStart expr ENDlambda -- ENDlambda is inserted for each production at next EOL
-- does not apply if enclosed in parens
| colon ExprCaseClause
LambdaStart ::= FunParams (‘=>’ | ‘?=>’)
| TypTypeParamClause ‘=>’
Quoted ::= ‘'’ ‘{’ Block ‘}’
Expand Down
4 changes: 4 additions & 0 deletions library/src/scala/language.scala
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,10 @@ object language {
@compileTimeOnly("`subCases` can only be used at compile time in import statements")
object subCases

/** Experimental support for single-line lambdas and case clause expressions after `:`
*/
@compileTimeOnly("`relaxedLambdaSyntax` can only be used at compile time in import statements")
object relaxedLambdaSyntax
}

/** The deprecated object contains features that are no longer officially suypported in Scala.
Expand Down
5 changes: 5 additions & 0 deletions library/src/scala/runtime/stdLibPatches/language.scala
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@ object language:
*/
@compileTimeOnly("`subCases` can only be used at compile time in import statements")
object subCases

/** Experimental support for single-line lambdas and case clause expressions after `:`
*/
@compileTimeOnly("`relaxedLambdaSyntax` can only be used at compile time in import statements")
object relaxedLambdaSyntax
end experimental

/** The deprecated object contains features that are no longer officially suypported in Scala.
Expand Down
59 changes: 59 additions & 0 deletions tests/neg/closure-args.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
-- [E040] Syntax Error: tests/neg/closure-args.scala:2:25 --------------------------------------------------------------
2 |val x = List(1).map: (x: => Int) => // error
| ^^
| an identifier expected, but '=>' found
|
| longer explanation available when compiling with `-explain`
-- Error: tests/neg/closure-args.scala:14:0 ----------------------------------------------------------------------------
14 | y => y > 0 // error // error
|^
|indented definitions expected, end of single-line lambda found
-- [E103] Syntax Error: tests/neg/closure-args.scala:14:4 --------------------------------------------------------------
14 | y => y > 0 // error // error
| ^
| Illegal start of toplevel definition
|
| longer explanation available when compiling with `-explain`
-- [E018] Syntax Error: tests/neg/closure-args.scala:18:20 -------------------------------------------------------------
18 |val e = xs.map: y => // error
| ^
| expression expected but end of single-line lambda found
|
| longer explanation available when compiling with `-explain`
-- [E040] Syntax Error: tests/neg/closure-args.scala:21:64 -------------------------------------------------------------
21 |val fs: List[List[Int] => Int] = xs.map: x => case y :: ys => y case Nil => -1 // error
| ^^^^
| end of single-line lambda expected, but 'case' found
-- [E008] Not Found Error: tests/neg/closure-args.scala:10:4 -----------------------------------------------------------
8 |val b: Int = xs
9 | .map: x => x
10 | * x // error
| ^
| value * is not a member of List[Int].
| Note that `*` is treated as an infix operator in Scala 3.
| If you do not want that, insert a `;` or empty line in front
| or drop any spaces behind the operator.
-- [E006] Not Found Error: tests/neg/closure-args.scala:16:21 ----------------------------------------------------------
16 |val c = List(xs.map: y => y + y) // error // error // error // error
| ^
| Not found: type y
|
| longer explanation available when compiling with `-explain`
-- [E006] Not Found Error: tests/neg/closure-args.scala:16:28 ----------------------------------------------------------
16 |val c = List(xs.map: y => y + y) // error // error // error // error
| ^
| Not found: type +
|
| longer explanation available when compiling with `-explain`
-- [E006] Not Found Error: tests/neg/closure-args.scala:16:26 ----------------------------------------------------------
16 |val c = List(xs.map: y => y + y) // error // error // error // error
| ^
| Not found: type y
|
| longer explanation available when compiling with `-explain`
-- [E006] Not Found Error: tests/neg/closure-args.scala:16:30 ----------------------------------------------------------
16 |val c = List(xs.map: y => y + y) // error // error // error // error
| ^
| Not found: type y
|
| longer explanation available when compiling with `-explain`
24 changes: 11 additions & 13 deletions tests/neg/closure-args.scala
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
import language.`3.3`

import language.experimental.relaxedLambdaSyntax
val x = List(1).map: (x: => Int) => // error
???
val z = List(1).map: + => // ok
???

val xs = List(1)
val b: Int = xs // error
.map: x => x * x // error
.filter: y => y > 0 // error
(0)
val d = xs // error
val b: Int = xs
.map: x => x
* x // error

val d = xs
.map: x => x.toString + xs.dropWhile:
y => y > 0
y => y > 0 // error // error

val c = List(xs.map: y => y + y) // error // error // error // error
val d2: String = xs // error
.map: x => x.toString + xs.dropWhile: y => y > 0 // error // error
.filter: z => !z.isEmpty // error
(0)

val fs: List[List[Int] => Int] = xs.map: x => case y :: ys => y case Nil => -1 // error // error
val e = xs.map: y => // error
y + 1

val fs: List[List[Int] => Int] = xs.map: x => case y :: ys => y case Nil => -1 // error
Loading
Loading