Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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 @@ -40,6 +40,7 @@ object Feature:
val packageObjectValues = experimental("packageObjectValues")
val multiSpreads = experimental("multiSpreads")
val subCases = experimental("subCases")
val relaxedLambdaSyntax = experimental("relaxedLambdaSyntax")

def experimentalAutoEnableFeatures(using Context): List[TermName] =
defn.languageExperimentalFeatures
Expand Down
133 changes: 90 additions & 43 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1090,27 +1090,56 @@ 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
}
lookahead.nextToken()
if lookahead.isIdent || lookahead.token == USCORE then
if lookahead.token == INDENT || lookahead.token == EOF then
Some(() => expr(Location.InColonArg))
else if in.featureEnabled(Feature.relaxedLambdaSyntax) then
isParamsAndArrow() match
case success @ Some(_) => success
case _ if !in.currentRegion.isInstanceOf[InParens] =>
Some: () =>
val t = inSepRegion(SingleLineLambda(_)):
expr(Location.InColonArg)
accept(ENDlambda)
t
case _ => None
else None
else None
def isParamsAndArrow(): Option[() => Tree] =
lookahead.nextToken()
isArrowIndent()
else if lookahead.token == LPAREN || lookahead.token == LBRACKET then
lookahead.skipParens()
isArrowIndent()
else false
if lookahead.isIdent || lookahead.token == USCORE then
lookahead.nextToken()
isArrowIndent()
else if lookahead.token == LPAREN || lookahead.token == LBRACKET then
lookahead.skipParens()
isArrowIndent()
else if lookahead.token == CASE && in.featureEnabled(Feature.relaxedLambdaSyntax) then
Some(() => singleCaseMatch())
else
None
isParamsAndArrow()
end followingIsLambdaAfterColon

/** Can the next lookahead token start an operand as defined by
* leadingOperandTokens, or is postfix ops enabled?
Expand Down Expand Up @@ -1175,12 +1204,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 @@ -1204,17 +1240,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 @@ -2358,6 +2396,7 @@ object Parsers {

/** Expr ::= [`implicit'] FunParams (‘=>’ | ‘?=>’) Expr
* | TypTypeParamClause ‘=>’ Expr
* | ExprCaseClause -- under experimental.relaxedLambdaSyntax
* | Expr1
* FunParams ::= Bindings
* | id
Expand Down Expand Up @@ -2409,6 +2448,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 @@ -2472,9 +2513,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 @@ -2769,8 +2809,10 @@ object Parsers {
* | SimpleExpr (TypeArgs | NamedTypeArgs)
* | SimpleExpr1 ArgumentExprs
* | SimpleExpr1 ColonArgument
* ColonArgument ::= colon [LambdaStart]
* ColonArgument ::= colon {LambdaStart}
* indent (CaseClauses | Block) outdent
* | colon LambdaStart {LambdaStart} expr ENDlambda -- under experimental.relaxedLambdaSyntax
* | colon ExprCaseClause -- under experimental.relaxedLambdaSyntax
* LambdaStart ::= FunParams (‘=>’ | ‘?=>’)
* | TypTypeParamClause ‘=>’
* ColonArgBody ::= indent (CaseClauses | Block) outdent
Expand Down Expand Up @@ -2853,12 +2895,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 @@ -3165,9 +3209,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 @@ -3184,6 +3228,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
6 changes: 5 additions & 1 deletion 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 @@ -293,8 +294,11 @@ SimpleExpr ::= SimpleRef
| SimpleExpr ColonArgument -- under language.experimental.fewerBraces
| SimpleExpr ‘_’ PostfixOp(expr, _) (to be dropped)
| XmlExpr -- to be dropped
ColonArgument ::= colon [LambdaStart]
ColonArgument ::= colon {LambdaStart}
indent (CaseClauses | Block) outdent
| colon LambdaStart {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
Loading
Loading