From f1d57c45efb2fb5cdeb58b63b14ae82a45a2040c Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 20 May 2022 14:51:00 +0200 Subject: [PATCH 01/15] Predict self type instead of converting after the fact --- .../dotty/tools/dotc/parsing/Parsers.scala | 61 +++++++++++++------ .../src/dotty/tools/dotc/parsing/Tokens.scala | 2 +- tests/neg/cycles.scala | 2 +- tests/neg/i4373b.scala | 2 +- tests/neg/parser-stability-16.scala | 1 - tests/neg/parser-stability-5.scala | 2 +- 6 files changed, 46 insertions(+), 24 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 3b7d11d16fd0..1b3d63a1625c 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -889,6 +889,16 @@ object Parsers { val next = in.lookahead.token next == LBRACKET || next == LPAREN + + def followingIsSelfType() = + val lookahead = in.LookaheadScanner() + lookahead.nextToken() + lookahead.token == COLON + && { + lookahead.nextToken() + canStartTypeTokens(lookahead.token) + } + /** Is current ident a `*`, and is it followed by a `)`, `, )`, `,EOF`? The latter two are not syntactically valid, but we need to include them here for error recovery. */ def followingIsVararg(): Boolean = @@ -3901,7 +3911,37 @@ object Parsers { stats.toList } - /** TemplateStatSeq ::= [id [`:' Type] `=>'] TemplateStat {semi TemplateStat} + /** SelfType ::= id [‘:’ InfixType] ‘=>’ + * | ‘this’ ‘:’ InfixType ‘=>’ + */ + def selfType(): ValDef = + if (in.isIdent || in.token == THIS) + && (in.lookahead.token == COLON && followingIsSelfType() + || in.lookahead.token == ARROW) + then + atSpan(in.offset) { + val selfName = + if in.token == THIS then + in.nextToken() + nme.WILDCARD + else ident() + val selfTpt = + if in.token == COLON then + in.nextToken() + infixType() + else + if selfName == nme.WILDCARD then accept(COLON) + TypeTree() + if in.token == ARROW then + in.token = SELFARROW // suppresses INDENT insertion after `=>` + in.nextToken() + else + syntaxError("`=>` expected after self type") + makeSelfDef(selfName, selfTpt) + } + else EmptyValDef + + /** TemplateStatSeq ::= [SelfType] TemplateStat {semi TemplateStat} * TemplateStat ::= Import * | Export * | Annotations Modifiers Def @@ -3913,25 +3953,8 @@ object Parsers { * | Annotations Modifiers EnumCase */ def templateStatSeq(): (ValDef, List[Tree]) = checkNoEscapingPlaceholders { - var self: ValDef = EmptyValDef val stats = new ListBuffer[Tree] - if isExprIntro && !isDefIntro(modifierTokens) then - val first = expr1() - if in.token == ARROW then - first match { - case Typed(tree @ This(EmptyTypeIdent), tpt) => - self = makeSelfDef(nme.WILDCARD, tpt).withSpan(first.span) - case _ => - val ValDef(name, tpt, _) = convertToParam(first, EmptyModifiers, "self type clause") - if (name != nme.ERROR) - self = makeSelfDef(name, tpt).withSpan(first.span) - } - in.token = SELFARROW // suppresses INDENT insertion after `=>` - in.nextToken() - else - stats += first - statSepOrEnd(stats) - end if + val self = selfType() while var empty = false if (in.token == IMPORT) diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index 1f414e7e912c..594312f9aaaa 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -233,7 +233,7 @@ object Tokens extends TokensCommon { final val canStartExprTokens2: TokenSet = canStartExprTokens3 | BitSet(DO) final val canStartTypeTokens: TokenSet = literalTokens | identifierTokens | BitSet( - THIS, SUPER, USCORE, LPAREN, AT) + THIS, SUPER, USCORE, LPAREN, LBRACE, AT) final val templateIntroTokens: TokenSet = BitSet(CLASS, TRAIT, OBJECT, ENUM, CASECLASS, CASEOBJECT) diff --git a/tests/neg/cycles.scala b/tests/neg/cycles.scala index b77d253bd527..b24a367c8377 100644 --- a/tests/neg/cycles.scala +++ b/tests/neg/cycles.scala @@ -38,6 +38,6 @@ class T2 { type U = X | Int } object T12 { - ??? : (T1 {})#U // old-error: conflicting bounds + val _ : (T1 {})#U = ??? // old-error: conflicting bounds ??? : (T2 {})#U // old-error: conflicting bounds } diff --git a/tests/neg/i4373b.scala b/tests/neg/i4373b.scala index 297fcd76ff08..45b60a46c721 100644 --- a/tests/neg/i4373b.scala +++ b/tests/neg/i4373b.scala @@ -1,5 +1,5 @@ // ==> 05bef7805687ba94da37177f7568e3ba7da1f91c.scala <== class x0 { - x1: // error + x1: x0 | _ // error // error \ No newline at end of file diff --git a/tests/neg/parser-stability-16.scala b/tests/neg/parser-stability-16.scala index 9ba58d0219f2..25fb38374c45 100644 --- a/tests/neg/parser-stability-16.scala +++ b/tests/neg/parser-stability-16.scala @@ -3,4 +3,3 @@ class x0[x0] { } trait x3 extends x0 { // error x1 = 0 object // error // error -// error \ No newline at end of file diff --git a/tests/neg/parser-stability-5.scala b/tests/neg/parser-stability-5.scala index 69f4568aab73..5de49927ee58 100644 --- a/tests/neg/parser-stability-5.scala +++ b/tests/neg/parser-stability-5.scala @@ -1,4 +1,4 @@ trait x0 { -x1 : { // error +x1 : { var x2 // error \ No newline at end of file From d64c6dcccbf24a6284ebe1b45cfb2eec8fa26865 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 20 May 2022 21:35:34 +0200 Subject: [PATCH 02/15] Better prediction of formal parameters of lambdas Instead of parsing all formal parameters of a lambda as expressions and converting to parameters at the end, we now scan ahead the first time we see a `:` to determine whether the list is followed by `=>` or `?=>`. If that's the case we parse this parameter and all following ones as bindings instead of expressions. --- .../dotty/tools/dotc/parsing/Parsers.scala | 50 ++++++++++++++++--- tests/neg/i13769.check | 12 ++--- tests/neg/i13769.scala | 2 +- tests/neg/i1424.scala | 2 +- tests/neg/i7818.scala | 2 +- 5 files changed, 51 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 1b3d63a1625c..c9c007cec1b4 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -444,9 +444,12 @@ object Parsers { /** Convert tree to formal parameter */ - def convertToParam(tree: Tree, mods: Modifiers, expected: String = "formal parameter"): ValDef = tree match { + def convertToParam(tree: Tree, mods: Modifiers, expected: String = "formal parameter"): ValDef = tree match + case param: ValDef => + param.withMods(param.mods | mods.flags) case id @ Ident(name) => makeParameter(name.asTermName, TypeTree(), mods, isBackquoted = isBackquoted(id)).withSpan(tree.span) + // the following three cases are needed only for 2.x parameters without enclosing parentheses case Typed(_, tpt: TypeBoundsTree) => syntaxError(s"not a legal $expected", tree.span) makeParameter(nme.ERROR, tree, mods) @@ -455,7 +458,6 @@ object Parsers { case _ => syntaxError(s"not a legal $expected", tree.span) makeParameter(nme.ERROR, tree, mods) - } /** Convert (qual)ident to type identifier */ @@ -913,6 +915,21 @@ object Parsers { } } + /** When encountering a `:`, is that in the first binding of a lambda? + * @pre location of the enclosing expression is `InParens`, so there is am open `(`. + */ + def followingisLambdaParams() = + val lookahead = in.LookaheadScanner() + lookahead.nextToken() + while lookahead.token != RPAREN && lookahead.token != EOF do + if lookahead.token == LPAREN then lookahead.skipParens() + else lookahead.nextToken() + lookahead.token == RPAREN + && { + lookahead.nextToken() + lookahead.isArrow + } + /* --------- OPERAND/OPERATOR STACK --------------------------------------- */ var opStack: List[OpInfo] = Nil @@ -2292,7 +2309,7 @@ object Parsers { placeholderParams = param :: placeholderParams atSpan(start) { Ident(pname) } case LPAREN => - atSpan(in.offset) { makeTupleOrParens(inParens(exprsInParensOpt())) } + atSpan(in.offset) { makeTupleOrParens(inParens(exprsInParensOrBindings())) } case LBRACE | INDENT => canApply = false blockExpr() @@ -2362,7 +2379,17 @@ object Parsers { val app = applyToClosure(t, in.offset, convertToParams(termIdent())) simpleExprRest(app, location, canApply = true) case _ => - t + t match + case id @ Ident(name) + if in.isColon() && location == Location.InParens && followingisLambdaParams() => + if name.is(WildcardParamName) then + assert(name == placeholderParams.head.name) + placeholderParams = placeholderParams.tail + atSpan(startOffset(id)) { + makeParameter(name.asTermName, typedOpt(), Modifiers(), isBackquoted = isBackquoted(id)) + } + case _ => + t } } @@ -2396,9 +2423,20 @@ object Parsers { } /** ExprsInParens ::= ExprInParens {`,' ExprInParens} + * Bindings ::= Binding {`,' Binding} */ - def exprsInParensOpt(): List[Tree] = - if (in.token == RPAREN) Nil else commaSeparated(exprInParens) + def exprsInParensOrBindings(): List[Tree] = + if in.token == RPAREN then Nil + else in.currentRegion.withCommasExpected { + var isFormalParams = false + def exprOrBinding() = + if isFormalParams then binding(Modifiers()) + else + val t = exprInParens() + if t.isInstanceOf[ValDef] then isFormalParams = true + t + commaSeparatedRest(exprOrBinding(), exprOrBinding) + } /** ParArgumentExprs ::= `(' [‘using’] [ExprsInParens] `)' * | `(' [ExprsInParens `,'] PostfixExpr `*' ')' diff --git a/tests/neg/i13769.check b/tests/neg/i13769.check index 3d7af1bd06a1..8291a84fc899 100644 --- a/tests/neg/i13769.check +++ b/tests/neg/i13769.check @@ -1,10 +1,6 @@ --- Error: tests/neg/i13769.scala:2:18 ---------------------------------------------------------------------------------- -2 |val te = tup.map((x: _ <: Int) => List(x)) // error // error - | ^^^^^^^^^^^ - | not a legal formal parameter --- [E006] Not Found Error: tests/neg/i13769.scala:2:39 ----------------------------------------------------------------- -2 |val te = tup.map((x: _ <: Int) => List(x)) // error // error - | ^ - | Not found: x +-- [E035] Syntax Error: tests/neg/i13769.scala:2:21 -------------------------------------------------------------------- +2 |val te = tup.map((x: _ <: Int) => List(x)) // error + | ^^^^^^^^ + | Unbound wildcard type | | longer explanation available when compiling with `-explain` diff --git a/tests/neg/i13769.scala b/tests/neg/i13769.scala index 67575e821334..e4e66fe2cef2 100644 --- a/tests/neg/i13769.scala +++ b/tests/neg/i13769.scala @@ -1,2 +1,2 @@ val tup = (1, "s") -val te = tup.map((x: _ <: Int) => List(x)) // error // error +val te = tup.map((x: _ <: Int) => List(x)) // error diff --git a/tests/neg/i1424.scala b/tests/neg/i1424.scala index 8eba3284211b..deffd671761e 100644 --- a/tests/neg/i1424.scala +++ b/tests/neg/i1424.scala @@ -1,3 +1,3 @@ class Test { - (x: Int) => x // error: not a legal self type clause // error: not found x + (x: Int) => x // error: not a legal self type clause } diff --git a/tests/neg/i7818.scala b/tests/neg/i7818.scala index 1dc243cfeedc..78cbee506784 100644 --- a/tests/neg/i7818.scala +++ b/tests/neg/i7818.scala @@ -1 +1 @@ -def foo = (x: @) => () // error // error \ No newline at end of file +def foo = (x: @) => () // error \ No newline at end of file From 421bdd660b0456c2ff1ae386f032c41bb1e0212a Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 20 May 2022 21:35:34 +0200 Subject: [PATCH 03/15] Change fewerBraces to always require colon --- .../dotty/tools/dotc/parsing/Parsers.scala | 145 +++++++++--------- .../dotty/tools/dotc/parsing/Scanners.scala | 14 +- docs/_docs/internals/syntax.md | 12 +- tests/neg-custom-args/nowarn/nowarn.check | 10 +- tests/neg-custom-args/nowarn/nowarn.scala | 8 +- tests/neg/closure-args.scala | 22 ++- tests/neg/i7751.scala | 2 +- tests/pos/closure-args.scala | 43 ++++-- tests/rewrites/rewrites.scala | 2 +- 9 files changed, 151 insertions(+), 107 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index c9c007cec1b4..1fddae5382ed 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -43,6 +43,7 @@ object Parsers { enum Location(val inParens: Boolean, val inPattern: Boolean, val inArgs: Boolean): case InParens extends Location(true, false, false) case InArgs extends Location(true, false, true) + case InColonArg extends Location(false, false, true) case InPattern extends Location(false, true, false) case InGuard extends Location(false, false, false) case InPatternArgs extends Location(false, true, true) // InParens not true, since it might be an alternative @@ -431,7 +432,7 @@ object Parsers { convertToParam(t, mods) :: Nil case Tuple(ts) => ts.map(convertToParam(_, mods)) - case t: Typed => + case t @ Typed(Ident(_), _) => report.errorOrMigrationWarning( em"parentheses are required around the parameter of a lambda${rewriteNotice()}", in.sourcePos(), from = `3.0`) @@ -444,20 +445,22 @@ object Parsers { /** Convert tree to formal parameter */ - def convertToParam(tree: Tree, mods: Modifiers, expected: String = "formal parameter"): ValDef = tree match - case param: ValDef => - param.withMods(param.mods | mods.flags) - case id @ Ident(name) => - makeParameter(name.asTermName, TypeTree(), mods, isBackquoted = isBackquoted(id)).withSpan(tree.span) - // the following three cases are needed only for 2.x parameters without enclosing parentheses - case Typed(_, tpt: TypeBoundsTree) => - syntaxError(s"not a legal $expected", tree.span) - makeParameter(nme.ERROR, tree, mods) - case Typed(id @ Ident(name), tpt) => - makeParameter(name.asTermName, tpt, mods, isBackquoted = isBackquoted(id)).withSpan(tree.span) - case _ => - syntaxError(s"not a legal $expected", tree.span) + def convertToParam(tree: Tree, mods: Modifiers): ValDef = + def fail() = + syntaxError(s"not a legal formal parameter for a function literal", tree.span) makeParameter(nme.ERROR, tree, mods) + tree match + case param: ValDef => + param.withMods(param.mods | mods.flags) + case id @ Ident(name) => + makeParameter(name.asTermName, TypeTree(), mods, isBackquoted = isBackquoted(id)).withSpan(tree.span) + // the following three cases are needed only for 2.x parameters without enclosing parentheses + case Typed(_, tpt: TypeBoundsTree) => + fail() + case Typed(id @ Ident(name), tpt) => + makeParameter(name.asTermName, tpt, mods, isBackquoted = isBackquoted(id)).withSpan(tree.span) + case _ => + fail() /** Convert (qual)ident to type identifier */ @@ -891,9 +894,8 @@ object Parsers { val next = in.lookahead.token next == LBRACKET || next == LPAREN - def followingIsSelfType() = - val lookahead = in.LookaheadScanner() + val lookahead = in.LookaheadScanner(allowIndent = true) lookahead.nextToken() lookahead.token == COLON && { @@ -915,10 +917,10 @@ object Parsers { } } - /** When encountering a `:`, is that in the first binding of a lambda? - * @pre location of the enclosing expression is `InParens`, so there is am open `(`. + /** When encountering a `:`, is that in the binding of a lambda? + * @pre location of the enclosing expression is `InParens`, so there is an open `(`. */ - def followingisLambdaParams() = + def followingIsLambdaParams() = val lookahead = in.LookaheadScanner() lookahead.nextToken() while lookahead.token != RPAREN && lookahead.token != EOF do @@ -930,6 +932,28 @@ object Parsers { lookahead.isArrow } + /** 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 = + val lookahead = in.LookaheadScanner(allowIndent = true) + def isArrowIndent() = + lookahead.isArrow + && { + lookahead.nextToken() + lookahead.token == INDENT + } + 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 + /* --------- OPERAND/OPERATOR STACK --------------------------------------- */ var opStack: List[OpInfo] = Nil @@ -2235,7 +2259,11 @@ object Parsers { in.nextToken() else accept(ARROW) - Function(params, if (location == Location.InBlock) block() else expr()) + val body = + if location == Location.InBlock then block() + else if location == Location.InColonArg && in.token == INDENT then blockExpr() + else expr() + Function(params, body) } /** PostfixExpr ::= InfixExpr [id [nl]] @@ -2285,9 +2313,11 @@ object Parsers { * | SimpleExpr `.` MatchClause * | SimpleExpr (TypeArgs | NamedTypeArgs) * | SimpleExpr1 ArgumentExprs - * | SimpleExpr1 `:` IndentedExpr -- under language.experimental.fewerBraces - * | SimpleExpr1 FunParams (‘=>’ | ‘?=>’) IndentedExpr -- under language.experimental.fewerBraces - * IndentedExpr ::= indent (CaseClauses | Block) outdent + * | SimpleExpr1 `:` ColonArgument -- under language.experimental.fewerBraces + * ColonArgument ::= indent (CaseClauses | Block) outdent + * | FunParams (‘=>’ | ‘?=>’) ColonArgBody + * | HkTypeParamClause ‘=>’ ColonArgBody + * ColonArgBody ::= indent (CaseClauses | Block) outdent * Quoted ::= ‘'’ ‘{’ Block ‘}’ * | ‘'’ ‘[’ Type ‘]’ */ @@ -2343,65 +2373,38 @@ object Parsers { simpleExprRest(t, location, canApply) } - def simpleExprRest(t: Tree, location: Location, canApply: Boolean = true): Tree = { + def simpleExprRest(t: Tree, location: Location, canApply: Boolean = true): Tree = if (canApply) argumentStart() - in.token match { + in.token match case DOT => in.nextToken() simpleExprRest(selectorOrMatch(t), location, canApply = true) case LBRACKET => val tapp = atSpan(startOffset(t), in.offset) { TypeApply(t, typeArgs(namedOK = true, wildOK = false)) } simpleExprRest(tapp, location, canApply = true) - case LPAREN if canApply => - val app = atSpan(startOffset(t), in.offset) { - val argExprs @ (args, isUsing) = argumentExprs() - if !isUsing && in.isArrow && location != Location.InGuard && in.fewerBracesEnabled then - val params = convertToParams(Tuple(args)) - if params.forall(_.name != nme.ERROR) then - applyToClosure(t, in.offset, params) - else - mkApply(t, argExprs) - else - mkApply(t, argExprs) - } - simpleExprRest(app, location, canApply = true) - case LBRACE | INDENT if canApply => + case LPAREN | LBRACE | INDENT if canApply => val app = atSpan(startOffset(t), in.offset) { mkApply(t, argumentExprs()) } simpleExprRest(app, location, canApply = true) case USCORE => - if in.lookahead.isArrow && location != Location.InGuard && in.fewerBracesEnabled then - val app = applyToClosure(t, in.offset, convertToParams(wildcardIdent())) - simpleExprRest(app, location, canApply = true) - else - atSpan(startOffset(t), in.skipToken()) { PostfixOp(t, Ident(nme.WILDCARD)) } - case IDENTIFIER - if !in.isOperator && in.lookahead.isArrow && location != Location.InGuard && in.fewerBracesEnabled => - val app = applyToClosure(t, in.offset, convertToParams(termIdent())) - simpleExprRest(app, location, canApply = true) + atSpan(startOffset(t), in.skipToken()) { PostfixOp(t, Ident(nme.WILDCARD)) } case _ => - t match - case id @ Ident(name) - if in.isColon() && location == Location.InParens && followingisLambdaParams() => - if name.is(WildcardParamName) then - assert(name == placeholderParams.head.name) - placeholderParams = placeholderParams.tail - atSpan(startOffset(id)) { - makeParameter(name.asTermName, typedOpt(), Modifiers(), isBackquoted = isBackquoted(id)) - } - case _ => - t - } - } - - def applyToClosure(t: Tree, start: Offset, params: List[ValDef]): Tree = - atSpan(startOffset(t), in.offset) { - val arg = atSpan(start, in.skipToken()) { - if in.token != INDENT then - syntaxErrorOrIncomplete(i"indented expression expected, ${in} found") - Function(params, blockExpr()) - } - Apply(t, arg) - } + if in.isColon() && location == Location.InParens && followingIsLambdaParams() then + t match + case id @ Ident(name) => + if name.is(WildcardParamName) then + assert(name == placeholderParams.head.name) + placeholderParams = placeholderParams.tail + atSpan(startOffset(id)) { + makeParameter(name.asTermName, typedOpt(), Modifiers(), isBackquoted = isBackquoted(id)) + } + case _ => t + else if in.fewerBracesEnabled && in.token == COLON && followingIsLambdaAfterColon() then + val app = atSpan(startOffset(t), in.skipToken()) { + Apply(t, expr(Location.InColonArg) :: Nil) + } + simpleExprRest(app, location, canApply = true) + else t + end simpleExprRest /** SimpleExpr ::= ‘new’ ConstrApp {`with` ConstrApp} [TemplateBody] * | ‘new’ TemplateBody diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 0718cc4b8748..af29bd3d003c 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -189,7 +189,10 @@ object Scanners { val indentSyntax = ((if (Config.defaultIndent) !noindentSyntax else ctx.settings.indent.value) || rewriteNoIndent) - && !isInstanceOf[LookaheadScanner] + && { this match + case self: LookaheadScanner => self.allowIndent + case _ => true + } if (rewrite) { val s = ctx.settings @@ -206,12 +209,17 @@ object Scanners { def featureEnabled(name: TermName) = Feature.enabled(name)(using languageImportContext) def erasedEnabled = featureEnabled(Feature.erasedDefinitions) + private inline val fewerBracesByDefault = false + // turn on to study impact on codebase if `fewerBraces` was the default + private var fewerBracesEnabledCache = false private var fewerBracesEnabledCtx: Context = NoContext def fewerBracesEnabled = if fewerBracesEnabledCtx ne myLanguageImportContext then - fewerBracesEnabledCache = featureEnabled(Feature.fewerBraces) + fewerBracesEnabledCache = + featureEnabled(Feature.fewerBraces) + || fewerBracesByDefault && indentSyntax fewerBracesEnabledCtx = myLanguageImportContext fewerBracesEnabledCache @@ -1067,7 +1075,7 @@ object Scanners { reset() next - class LookaheadScanner() extends Scanner(source, offset) { + class LookaheadScanner(val allowIndent: Boolean = false) extends Scanner(source, offset) { override def languageImportContext = Scanner.this.languageImportContext } diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index 55b098e91849..15f7c8e4d31b 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -253,13 +253,13 @@ SimpleExpr ::= SimpleRef | SimpleExpr ‘.’ MatchClause | SimpleExpr TypeArgs TypeApply(expr, args) | SimpleExpr ArgumentExprs Apply(expr, args) - | SimpleExpr ‘:’ IndentedExpr -- under language.experimental.fewerBraces - | SimpleExpr FunParams (‘=>’ | ‘?=>’) IndentedExpr -- under language.experimental.fewerBraces + | SimpleExpr ‘:’ ColonArgument -- under language.experimental.fewerBraces | SimpleExpr ‘_’ PostfixOp(expr, _) (to be dropped) - | XmlExpr -- to be dropped -IndentedExpr ::= indent CaseClauses | Block outdent -Quoted ::= ‘'’ ‘{’ Block ‘}’ - | ‘'’ ‘[’ Type ‘]’ + | XmlExpr -- to be dropped +ColonArgument ::= indent CaseClauses | Block outdent + | FunParams (‘=>’ | ‘?=>’) ColonArgBody + | HkTypeParamClause ‘=>’ ColonArgBody +ColonArgBody ::= indent (CaseClauses | Block) outdent ExprSplice ::= spliceId -- if inside quoted block | ‘$’ ‘{’ Block ‘}’ -- unless inside quoted pattern | ‘$’ ‘{’ Pattern ‘}’ -- when inside quoted pattern diff --git a/tests/neg-custom-args/nowarn/nowarn.check b/tests/neg-custom-args/nowarn/nowarn.check index 5fd085624254..232ea1a3a05f 100644 --- a/tests/neg-custom-args/nowarn/nowarn.check +++ b/tests/neg-custom-args/nowarn/nowarn.check @@ -63,7 +63,7 @@ Matching filters for @nowarn or -Wconf: | ^ | method f is deprecated -- Deprecation Warning: tests/neg-custom-args/nowarn/nowarn.scala:47:10 ------------------------------------------------ -47 |def t7c = f: // warning (deprecation) +47 |def t7c = f // warning (deprecation) | ^ | method f is deprecated -- Unchecked Warning: tests/neg-custom-args/nowarn/nowarn.scala:53:7 --------------------------------------------------- @@ -78,10 +78,10 @@ Matching filters for @nowarn or -Wconf: 40 |@nowarn("msg=fish") def t6d = f // error (unused nowarn), warning (deprecation) |^^^^^^^^^^^^^^^^^^^ |@nowarn annotation does not suppress any warnings --- Error: tests/neg-custom-args/nowarn/nowarn.scala:48:3 --------------------------------------------------------------- -48 | @nowarn("msg=fish") // error (unused nowarn) - | ^^^^^^^^^^^^^^^^^^^ - | @nowarn annotation does not suppress any warnings +-- Error: tests/neg-custom-args/nowarn/nowarn.scala:48:5 --------------------------------------------------------------- +48 | : @nowarn("msg=fish") // error (unused nowarn) + | ^^^^^^^^^^^^^^^^^^^ + | @nowarn annotation does not suppress any warnings -- Error: tests/neg-custom-args/nowarn/nowarn.scala:60:0 --------------------------------------------------------------- 60 |@nowarn def t9a = { 1: @nowarn; 2 } // error (outer @nowarn is unused) |^^^^^^^ diff --git a/tests/neg-custom-args/nowarn/nowarn.scala b/tests/neg-custom-args/nowarn/nowarn.scala index 252838767e30..f5d10a5f262a 100644 --- a/tests/neg-custom-args/nowarn/nowarn.scala +++ b/tests/neg-custom-args/nowarn/nowarn.scala @@ -42,10 +42,10 @@ def t6a = f // warning (refchecks, deprecation) @nowarn def t6f = f def t7a = f: @nowarn("cat=deprecation") -def t7b = f: - @nowarn("msg=deprecated") -def t7c = f: // warning (deprecation) - @nowarn("msg=fish") // error (unused nowarn) +def t7b = f + : @nowarn("msg=deprecated") +def t7c = f // warning (deprecation) + : @nowarn("msg=fish") // error (unused nowarn) def t7d = f: @nowarn("") def t7e = f: @nowarn diff --git a/tests/neg/closure-args.scala b/tests/neg/closure-args.scala index 9b59de205d86..333cda40cfd6 100644 --- a/tests/neg/closure-args.scala +++ b/tests/neg/closure-args.scala @@ -1,9 +1,25 @@ import language.experimental.fewerBraces -val x = List().map (x: => Int) => // error +val x = List().map: (x: => Int) => // error ??? -val y = List() map x => // error +val y = List() map: x => // error x + 1 // error -val z = List() map + => // error +val z = List().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 + .map: x => x.toString + xs.dropWhile: + y => y > 0 + +val c = List(xs.map: y => y + y) // 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 diff --git a/tests/neg/i7751.scala b/tests/neg/i7751.scala index ed33723a152d..4c835a533704 100644 --- a/tests/neg/i7751.scala +++ b/tests/neg/i7751.scala @@ -1,3 +1,3 @@ import language.experimental.fewerBraces -val a = Some(a=a,)=> // error // error // error +val a = Some(a=a,)=> // error // error val a = Some(x=y,)=> diff --git a/tests/pos/closure-args.scala b/tests/pos/closure-args.scala index dd30baf7d2b0..1991cd1522b0 100644 --- a/tests/pos/closure-args.scala +++ b/tests/pos/closure-args.scala @@ -1,16 +1,33 @@ import language.experimental.fewerBraces -val xs = List(1, 2, 3) -val ys = xs.map x => - x + 1 -val x = ys.foldLeft(0) (x, y) => - x + y -val y = ys.foldLeft(0) (x: Int, y: Int) => - val z = x + y - z * z -val as: Int = xs - .map x => - x * x - .filter y => +object Test1: + val xs = List(1, 2, 3) + val ys = xs.map: x => + x + 1 + val x = ys.foldLeft(0): (x, y) => + x + y + val y = ys.foldLeft(0): (x: Int, y: Int) => + val z = x + y + z * z + val a: Int = xs + .map: x => + x * x + .filter: (y: Int) => + y > 0 + (0) + val e = xs.map: + case 1 => 2 + case 2 => 3 + case x => x + .filter: + x => x > 0 + + extension (xs: List[Int]) def foo(f: [X] => X => X) = () + + val p = xs.foo: + [X] => (x: X) => x + + val q = (x: String => String) => x + + val r = x < 0 && : y > 0 - (0) diff --git a/tests/rewrites/rewrites.scala b/tests/rewrites/rewrites.scala index 6e2e4c2c8d9f..f38269bd0ed6 100644 --- a/tests/rewrites/rewrites.scala +++ b/tests/rewrites/rewrites.scala @@ -56,7 +56,7 @@ object test3 { } } } - def g = { x: Int => + def g = { (x: Int) => x + 1 } } From a112f675e0c0fa23dfe6b8e58a492a6c4092e6cf Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 23 May 2022 16:28:16 +0200 Subject: [PATCH 04/15] Update doc page --- .../reference/experimental/fewer-braces.md | 36 +++++++++---------- tests/pos/no-selftype.scala | 7 ++++ 2 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 tests/pos/no-selftype.scala diff --git a/docs/_docs/reference/experimental/fewer-braces.md b/docs/_docs/reference/experimental/fewer-braces.md index a6275e73a413..7a8f5034de04 100644 --- a/docs/_docs/reference/experimental/fewer-braces.md +++ b/docs/_docs/reference/experimental/fewer-braces.md @@ -12,7 +12,8 @@ import language.experimental.fewerBraces ``` Alternatively, it can be enabled with command line option `-language:experimental.fewerBraces`. -This variant is more contentious and less stable than the rest of the significant indentation scheme. It allows to replace a function argument in braces by a `:` at the end of a line and indented code, similar to the convention for class bodies. It also allows to leave out braces around arguments that are multi-line function values. +This variant is more contentious and less stable than the rest of the significant indentation scheme. It allows to replace a function argument in braces by a `:` at the end of a line and indented code, similar to the convention for class bodies. The `:` can +optionally be followed by the parameter part of a function literal. ## Using `:` At End Of Line @@ -50,34 +51,31 @@ val firstLine = files.get(fileName).fold: ## Lambda Arguments Without Braces -Braces can also be omitted around multiple line function value arguments: +The `:` can optionally be followed by the parameter part of a function literal: ```scala -val xs = elems.map x => +val xs = elems.map: x => val y = x - 1 y * y xs.foldLeft (x, y) => x + y ``` -Braces can be omitted if the lambda starts with a parameter list and `=>` or `=>?` at the end of one line and it has an indented body on the following lines. +Braces can be omitted if the lambda starts with a parameter list and an arrow symbol `=>` or `?=>`. +The arrow is followed on the next line(s) by the body of the functional literal which must be indented +relative to the previous line. ## Syntax Changes +As a lexical change, a `:` at the end of a line is now always treated as a +"colon at end of line" token. + +The context free grammar changes as follows: ``` SimpleExpr ::= ... - | SimpleExpr `:` IndentedArgument - | SimpleExpr FunParams (‘=>’ | ‘?=>’) IndentedArgument -InfixExpr ::= ... - | InfixExpr id `:` IndentedArgument -IndentedArgument ::= indent (CaseClauses | Block) outdent -``` - -Note that a lambda argument must have the `=>` at the end of a line for braces -to be optional. For instance, the following would also be incorrect: + | SimpleExpr ‘:’ ColonArgument -```scala - xs.map x => x + 1 // error: braces or parentheses are required -``` -The lambda has to be enclosed in braces or parentheses: -```scala - xs.map(x => x + 1) // ok + | SimpleExpr FunParams (‘=>’ | ‘?=>’) IndentedArgument +ColonArgument ::= indent CaseClauses | Block outdent + | FunParams (‘=>’ | ‘?=>’) ColonArgBody + | HkTypeParamClause ‘=>’ ColonArgBody +ColonArgBody ::= indent (CaseClauses | Block) outdent ``` diff --git a/tests/pos/no-selftype.scala b/tests/pos/no-selftype.scala new file mode 100644 index 000000000000..6774aafd36b3 --- /dev/null +++ b/tests/pos/no-selftype.scala @@ -0,0 +1,7 @@ +import language.experimental.fewerBraces +object f: + def apply(x: Int) = println(x) + +class C: + f: + 22 From c1f18cbabd829ebef179e06aeba2a5d4237047c7 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 24 May 2022 11:45:35 +0200 Subject: [PATCH 05/15] Tigthen lexical rules for ColonAtEOL --- .../dotty/tools/dotc/parsing/Scanners.scala | 10 +-- .../src/dotty/tools/dotc/parsing/Tokens.scala | 2 + tests/neg/indent-colons.check | 61 +++++++++++++++++++ tests/neg/indent-colons.scala | 34 +++++++++++ tests/pos/closure-args.scala | 3 +- tests/pos/i12218.scala | 2 +- tests/pos/indent-colons.scala | 26 +++++--- 7 files changed, 122 insertions(+), 16 deletions(-) create mode 100644 tests/neg/indent-colons.check create mode 100644 tests/neg/indent-colons.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index af29bd3d003c..5ffe8089e70c 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -397,7 +397,7 @@ object Scanners { adjustSepRegions(lastToken) getNextToken(lastToken) if isAfterLineEnd then handleNewLine(lastToken) - postProcessToken() + postProcessToken(lastToken) printState() final def printState() = @@ -694,7 +694,7 @@ object Scanners { * SEMI + ELSE => ELSE, COLON + => COLONEOL * - Insert missing OUTDENTs at EOF */ - def postProcessToken(): Unit = { + def postProcessToken(lastToken: Token): Unit = { def fuse(tok: Int) = { token = tok offset = prev.offset @@ -730,7 +730,8 @@ object Scanners { case END => if !isEndMarker then token = IDENTIFIER case COLON => - if fewerBracesEnabled then observeColonEOL() + if fewerBracesEnabled && colonEOLPredecessors.contains(lastToken) && lastOffset == offset then + observeColonEOL() case RBRACE | RPAREN | RBRACKET => closeIndented() case EOF => @@ -1526,7 +1527,8 @@ object Scanners { case NEWLINE => ";" case NEWLINES => ";;" case COMMA => "," - case _ => showToken(token) + case _ => + if debugTokenStream then showTokenDetailed(token) else showToken(token) } /* Resume normal scanning after XML */ diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index 594312f9aaaa..94c9f593849a 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -287,6 +287,8 @@ object Tokens extends TokensCommon { final val endMarkerTokens = identifierTokens | BitSet(IF, WHILE, FOR, MATCH, TRY, NEW, THROW, GIVEN, VAL, THIS) + final val colonEOLPredecessors = identifierTokens | BitSet(RPAREN, RBRACKET) + final val closingParens = BitSet(RPAREN, RBRACKET, RBRACE) final val softModifierNames = Set(nme.inline, nme.opaque, nme.open, nme.transparent, nme.infix) diff --git a/tests/neg/indent-colons.check b/tests/neg/indent-colons.check new file mode 100644 index 000000000000..06bd7a31b079 --- /dev/null +++ b/tests/neg/indent-colons.check @@ -0,0 +1,61 @@ +-- Error: tests/neg/indent-colons.scala:6:4 ---------------------------------------------------------------------------- +6 | : // error + | ^ + | end of statement expected but ':' found +-- Error: tests/neg/indent-colons.scala:12:2 --------------------------------------------------------------------------- +12 | : // error + | ^ + | end of statement expected but ':' found +-- Error: tests/neg/indent-colons.scala:19:2 --------------------------------------------------------------------------- +19 | : // error + | ^ + | end of statement expected but ':' found +-- [E018] Syntax Error: tests/neg/indent-colons.scala:26:14 ------------------------------------------------------------ +26 | val y = 1 + : // error + | ^ + | expression expected but : found + | + | longer explanation available when compiling with `-explain` +-- [E018] Syntax Error: tests/neg/indent-colons.scala:30:27 ------------------------------------------------------------ +30 | val all = credentials ++ : // error + | ^ + | expression expected but : found + | + | longer explanation available when compiling with `-explain` +-- [E134] Type Error: tests/neg/indent-colons.scala:23:12 -------------------------------------------------------------- +23 | val x = 1.+ : // error + | ^^^ + | None of the overloaded alternatives of method + in class Int with types + | (x: Double): Double + | (x: Float): Float + | (x: Long): Long + | (x: Int): Int + | (x: Char): Int + | (x: Short): Int + | (x: Byte): Int + | (x: String): String + | match expected type (2 : Int) +-- [E006] Not Found Error: tests/neg/indent-colons.scala:32:7 ---------------------------------------------------------- +32 | if file.isEmpty // error + | ^^^^ + | Not found: file + | + | longer explanation available when compiling with `-explain` +-- [E006] Not Found Error: tests/neg/indent-colons.scala:34:13 --------------------------------------------------------- +34 | else Seq(file) // error + | ^^^^ + | Not found: file + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg/indent-colons.scala:4:2 ---------------------------------------------------------------------------- +4 | tryEither: // error + | ^^^^^^^^^ + | missing arguments for method tryEither +-- Error: tests/neg/indent-colons.scala:10:2 --------------------------------------------------------------------------- +10 | tryEither: // error + | ^^^^^^^^^ + | missing arguments for method tryEither +-- Error: tests/neg/indent-colons.scala:17:2 --------------------------------------------------------------------------- +17 | Some(3).fold: // error + | ^^^^^^^^^^^^ + | missing arguments for method fold in class Option diff --git a/tests/neg/indent-colons.scala b/tests/neg/indent-colons.scala new file mode 100644 index 000000000000..5364713dd4aa --- /dev/null +++ b/tests/neg/indent-colons.scala @@ -0,0 +1,34 @@ +def tryEither[T](x: T)(y: Int => T): T = ??? + +def test1 = + tryEither: // error + "hello" + : // error + y => y.toString + +def test2 = + tryEither: // error + "hello" + : // error + _.toString + + +val o = + Some(3).fold: // error + "nothing" + : // error + x => x.toString + +object Test23: + val x = 1.+ : // error + 2 + + val y = 1 + : // error + x + + val credentials = List("OK") + val all = credentials ++ : // error + val file = "file" + if file.isEmpty // error + then Seq("none") + else Seq(file) // error \ No newline at end of file diff --git a/tests/pos/closure-args.scala b/tests/pos/closure-args.scala index 1991cd1522b0..abcf78dc2e97 100644 --- a/tests/pos/closure-args.scala +++ b/tests/pos/closure-args.scala @@ -29,5 +29,6 @@ object Test1: val q = (x: String => String) => x - val r = x < 0 && : + val r = x < 0 && locally: y > 0 + diff --git a/tests/pos/i12218.scala b/tests/pos/i12218.scala index 515e71f83fc8..da1e1bc61184 100644 --- a/tests/pos/i12218.scala +++ b/tests/pos/i12218.scala @@ -3,7 +3,7 @@ import language.experimental.fewerBraces val arr = Array(1,2,3) if arr.isEmpty - || : + || locally: val first = arr(0) first != 1 then println("invalid arr") diff --git a/tests/pos/indent-colons.scala b/tests/pos/indent-colons.scala index 09abc69a483a..b839568a7aff 100644 --- a/tests/pos/indent-colons.scala +++ b/tests/pos/indent-colons.scala @@ -122,35 +122,41 @@ def tryEither[T](x: T)(y: Int => T): T = ??? def test1 = tryEither: - "hello" - : - y => y.toString + "hello" + .apply: + y => y.toString def test2 = tryEither: "hello" - : + .apply: _.toString val o = Some(3).fold: "nothing" - : + .apply: x => x.toString object Test23: - val x = 1.+ : // ok - 2 - val y = 1 + : // ok + transparent inline def nested[T](inline x: T): T = x + + val x = (1.+): 2 + val y = 1 + nested: // ok + x + + val _ = 1 `+`: // ok + x + val r = 1 to: 100 val credentials = List("OK") - val all = credentials ++ : + val all = credentials ++ nested: val file = "file" if file.isEmpty then Seq("none") @@ -162,7 +168,7 @@ extension (x: Boolean) def test24(x: Int, y: Int) = x < y or: x > y - or: + `or`: x == y From d3670a658839c55523124bd8b07f0940935be4fb Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 24 May 2022 17:25:40 +0200 Subject: [PATCH 06/15] Recognize fewer braces COLONeol's depending on previous token - Drop the criterion of no whitespace. - Also, refactor the logic so that tokens are converted to COLONeol only on demand. --- .../tools/dotc/parsing/JavaScanners.scala | 2 +- .../dotty/tools/dotc/parsing/Parsers.scala | 33 ++++++------ .../dotty/tools/dotc/parsing/Scanners.scala | 51 ++++++++++--------- .../src/dotty/tools/dotc/parsing/Tokens.scala | 27 +++++----- 4 files changed, 61 insertions(+), 52 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/JavaScanners.scala b/compiler/src/dotty/tools/dotc/parsing/JavaScanners.scala index d7178bd411e7..03d319ce6e70 100644 --- a/compiler/src/dotty/tools/dotc/parsing/JavaScanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/JavaScanners.scala @@ -179,7 +179,7 @@ object JavaScanners { nextChar() case ':' => - token = COLON + token = COLONop nextChar() case '@' => diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 1fddae5382ed..3eaf5ed5970a 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -312,7 +312,7 @@ object Parsers { def acceptColon(): Int = val offset = in.offset if in.isColon() then { in.nextToken(); offset } - else accept(COLON) + else accept(COLONop) /** semi = nl {nl} | `;' * nl = `\n' // where allowed @@ -897,10 +897,11 @@ object Parsers { def followingIsSelfType() = val lookahead = in.LookaheadScanner(allowIndent = true) lookahead.nextToken() - lookahead.token == COLON + lookahead.token == COLONfollow && { + lookahead.observeColonEOL(inTemplate = false) lookahead.nextToken() - canStartTypeTokens(lookahead.token) + canStartTypeTokens.contains(lookahead.token) } /** Is current ident a `*`, and is it followed by a `)`, `, )`, `,EOF`? The latter two are not @@ -1310,7 +1311,8 @@ object Parsers { def colonAtEOLOpt(): Unit = { possibleColonOffset = in.lastOffset - if in.token == COLONEOL then in.nextToken() + in.observeColonEOL(inTemplate = false) + if in.token == COLONeol then in.nextToken() } def argumentStart(): Unit = @@ -1326,8 +1328,8 @@ object Parsers { patch(source, Span(in.offset), " ") def possibleTemplateStart(isNew: Boolean = false): Unit = - in.observeColonEOL() - if in.token == COLONEOL then + in.observeColonEOL(inTemplate = true) + if in.token == COLONeol then if in.lookahead.token == END then in.token = NEWLINE else in.nextToken() @@ -2127,7 +2129,7 @@ object Parsers { } case _ => t - case COLON => + case COLONop | COLONfollow => in.nextToken() ascription(t, location) case _ => @@ -2216,7 +2218,7 @@ object Parsers { val start = in.offset val name = bindingName() val t = - if (in.token == COLON && location == Location.InBlock) { + if ((in.token == COLONop || in.token == COLONfollow) && location == Location.InBlock) { report.errorOrMigrationWarning( s"This syntax is no longer supported; parameter needs to be enclosed in (...)${rewriteNotice(`future-migration`)}", source.atSpan(Span(start, in.lastOffset)), @@ -2398,7 +2400,7 @@ object Parsers { makeParameter(name.asTermName, typedOpt(), Modifiers(), isBackquoted = isBackquoted(id)) } case _ => t - else if in.fewerBracesEnabled && in.token == COLON && followingIsLambdaAfterColon() then + else if in.fewerBracesEnabled && in.token == COLONfollow && followingIsLambdaAfterColon() then val app = atSpan(startOffset(t), in.skipToken()) { Apply(t, expr(Location.InColonArg) :: Nil) } @@ -2416,7 +2418,6 @@ object Parsers { val parents = if in.isNestedStart then Nil else constrApps(exclude = COMMA) - colonAtEOLOpt() possibleTemplateStart(isNew = true) parents match { case parent :: Nil if !in.isNestedStart => @@ -2726,7 +2727,7 @@ object Parsers { */ def pattern1(location: Location = Location.InPattern): Tree = val p = pattern2() - if in.token == COLON then + if in.token == COLONop || in.token == COLONfollow then in.nextToken() ascription(p, location) else p @@ -3836,7 +3837,7 @@ object Parsers { val parents = if (in.token == EXTENDS) { in.nextToken() - if (in.token == LBRACE || in.token == COLONEOL) { + if (in.token == LBRACE || in.token == COLONeol) { report.errorOrMigrationWarning( "`extends` must be followed by at least one parent", in.sourcePos(), from = `3.0`) @@ -3957,8 +3958,8 @@ object Parsers { */ def selfType(): ValDef = if (in.isIdent || in.token == THIS) - && (in.lookahead.token == COLON && followingIsSelfType() - || in.lookahead.token == ARROW) + && in.lookahead.token == COLONop && followingIsSelfType() + || in.lookahead.token == ARROW then atSpan(in.offset) { val selfName = @@ -3967,11 +3968,11 @@ object Parsers { nme.WILDCARD else ident() val selfTpt = - if in.token == COLON then + if in.token == COLONfollow then in.nextToken() infixType() else - if selfName == nme.WILDCARD then accept(COLON) + if selfName == nme.WILDCARD then accept(COLONfollow) TypeTree() if in.token == ARROW then in.token = SELFARROW // suppresses INDENT insertion after `=>` diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 5ffe8089e70c..41e469eb9c10 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -75,13 +75,13 @@ object Scanners { def isNestedStart = token == LBRACE || token == INDENT def isNestedEnd = token == RBRACE || token == OUTDENT - /** Is token a COLON, after having converted COLONEOL to COLON? + /** Is token a COLON, after having converted COLONeol to COLON? * The conversion means that indentation is not significant after `:` * anymore. So, warning: this is a side-effecting operation. */ def isColon() = - if token == COLONEOL then token = COLON - token == COLON + if token == COLONeol then token = COLONop + token == COLONop || token == COLONfollow /** Is current token first one after a newline? */ def isAfterLineEnd: Boolean = lineOffset >= 0 @@ -394,10 +394,11 @@ object Scanners { */ def nextToken(): Unit = val lastToken = token + val lastName = name adjustSepRegions(lastToken) getNextToken(lastToken) if isAfterLineEnd then handleNewLine(lastToken) - postProcessToken(lastToken) + postProcessToken(lastToken, lastName) printState() final def printState() = @@ -428,7 +429,7 @@ object Scanners { && { // Is current lexeme assumed to start an expression? // This is the case if the lexime is one of the tokens that - // starts an expression or it is a COLONEOL. Furthermore, if + // starts an expression or it is a COLONeol. Furthermore, if // the previous token is in backticks, the lexeme may not be a binary operator. // I.e. in // @@ -439,7 +440,7 @@ object Scanners { // in backticks and is a binary operator. Hence, `x` is not classified as a // leading infix operator. def assumeStartsExpr(lexeme: TokenData) = - (canStartExprTokens.contains(lexeme.token) || lexeme.token == COLONEOL) + (canStartExprTokens.contains(lexeme.token) || lexeme.token == COLONeol) && (!lexeme.isOperator || nme.raw.isUnary(lexeme.name)) val lookahead = LookaheadScanner() lookahead.allowLeadingInfixOperators = false @@ -615,12 +616,11 @@ object Scanners { currentRegion match case r: Indented => insert(OUTDENT, offset) - if next.token != COLON then - handleNewIndentWidth(r.enclosing, ir => - errorButContinue( - i"""The start of this line does not match any of the previous indentation widths. - |Indentation width of current line : $nextWidth - |This falls between previous widths: ${ir.width} and $lastWidth""")) + handleNewIndentWidth(r.enclosing, ir => + errorButContinue( + i"""The start of this line does not match any of the previous indentation widths. + |Indentation width of current line : $nextWidth + |This falls between previous widths: ${ir.width} and $lastWidth""")) case r => if skipping then if r.enclosing.isClosedByUndentAt(nextWidth) then @@ -637,7 +637,7 @@ object Scanners { currentRegion.knownWidth = nextWidth else if (lastWidth != nextWidth) errorButContinue(spaceTabMismatchMsg(lastWidth, nextWidth)) - if token != OUTDENT || next.token == COLON then + if token != OUTDENT then handleNewIndentWidth(currentRegion, _.otherIndentWidths += nextWidth) end handleNewLine @@ -646,19 +646,22 @@ object Scanners { |Previous indent : $lastWidth |Latest indent : $nextWidth""" - def observeColonEOL(): Unit = - if token == COLON then + def observeColonEOL(inTemplate: Boolean): Unit = + val enabled = + if inTemplate then token == COLONop || token == COLONfollow + else token == COLONfollow && fewerBracesEnabled + if enabled then lookAhead() val atEOL = isAfterLineEnd || token == EOF reset() - if atEOL then token = COLONEOL + if atEOL then token = COLONeol def observeIndented(): Unit = if indentSyntax && isNewLine then val nextWidth = indentWidth(next.offset) val lastWidth = currentRegion.indentWidth if lastWidth < nextWidth then - currentRegion = Indented(nextWidth, COLONEOL, currentRegion) + currentRegion = Indented(nextWidth, COLONeol, currentRegion) offset = next.offset token = INDENT end observeIndented @@ -691,10 +694,10 @@ object Scanners { case _ => /** - Join CASE + CLASS => CASECLASS, CASE + OBJECT => CASEOBJECT - * SEMI + ELSE => ELSE, COLON + => COLONEOL + * SEMI + ELSE => ELSE, COLON following id/)/] => COLONfollow * - Insert missing OUTDENTs at EOF */ - def postProcessToken(lastToken: Token): Unit = { + def postProcessToken(lastToken: Token, lastName: SimpleName): Unit = { def fuse(tok: Int) = { token = tok offset = prev.offset @@ -729,9 +732,10 @@ object Scanners { reset() case END => if !isEndMarker then token = IDENTIFIER - case COLON => - if fewerBracesEnabled && colonEOLPredecessors.contains(lastToken) && lastOffset == offset then - observeColonEOL() + case COLONop => + if lastToken == IDENTIFIER && lastName != null && isIdentifierStart(lastName.head) + || colonEOLPredecessors.contains(lastToken) + then token = COLONfollow case RBRACE | RPAREN | RBRACKET => closeIndented() case EOF => @@ -1188,7 +1192,7 @@ object Scanners { isSoftModifier && inModifierPosition() def isSoftModifierInParamModifierPosition: Boolean = - isSoftModifier && lookahead.token != COLON + isSoftModifier && lookahead.token != COLONop && lookahead.token != COLONfollow def isErased: Boolean = isIdent(nme.erased) && erasedEnabled @@ -1527,6 +1531,7 @@ object Scanners { case NEWLINE => ";" case NEWLINES => ";;" case COMMA => "," + case COLONfollow | COLONeol => "':'" case _ => if debugTokenStream then showTokenDetailed(token) else showToken(token) } diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index 94c9f593849a..9fd4759db9fc 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -16,13 +16,6 @@ abstract class TokensCommon { def tokenRange(lo: Int, hi: Int): TokenSet = BitSet(lo to hi: _*) - def showTokenDetailed(token: Int): String = debugString(token) - - def showToken(token: Int): String = { - val str = tokenString(token) - if (isKeyword(token)) s"'$str'" else str - } - val tokenString, debugString: Array[String] = new Array[String](maxToken + 1) def enter(token: Int, str: String, debugStr: String = ""): Unit = { @@ -107,7 +100,7 @@ abstract class TokensCommon { /** special keywords */ //inline val USCORE = 73; enter(USCORE, "_") - inline val COLON = 74; enter(COLON, ":") + inline val COLONop = 74; enter(COLONop, ":") // a stand-alone `:`, see also COLONfollow inline val EQUALS = 75; enter(EQUALS, "=") //inline val LARROW = 76; enter(LARROW, "<-") //inline val ARROW = 77; enter(ARROW, "=>") @@ -204,8 +197,11 @@ object Tokens extends TokensCommon { inline val QUOTE = 87; enter(QUOTE, "'") - inline val COLONEOL = 88; enter(COLONEOL, ":", ": at eol") - inline val SELFARROW = 89; enter(SELFARROW, "=>") // reclassified ARROW following self-type + inline val COLONfollow = 88; enter(COLONfollow, ":") + // A `:` following an alphanumeric identifier or one of the tokens in colonEOLPredecessors + inline val COLONeol = 89; enter(COLONeol, ":", ": at eol") + // A `:` recognized as starting an indentation block + inline val SELFARROW = 90; enter(SELFARROW, "=>") // reclassified ARROW following self-type /** XML mode */ inline val XMLSTART = 99; enter(XMLSTART, "$XMLSTART$<") // TODO: deprecate @@ -276,7 +272,7 @@ object Tokens extends TokensCommon { final val closingRegionTokens = BitSet(RBRACE, RPAREN, RBRACKET, CASE) | statCtdTokens final val canStartIndentTokens: BitSet = - statCtdTokens | BitSet(COLONEOL, WITH, EQUALS, ARROW, CTXARROW, LARROW, WHILE, TRY, FOR, IF, THROW, RETURN) + statCtdTokens | BitSet(COLONeol, WITH, EQUALS, ARROW, CTXARROW, LARROW, WHILE, TRY, FOR, IF, THROW, RETURN) /** Faced with the choice between a type and a formal parameter, the following * tokens determine it's a formal parameter. @@ -287,9 +283,16 @@ object Tokens extends TokensCommon { final val endMarkerTokens = identifierTokens | BitSet(IF, WHILE, FOR, MATCH, TRY, NEW, THROW, GIVEN, VAL, THIS) - final val colonEOLPredecessors = identifierTokens | BitSet(RPAREN, RBRACKET) + final val colonEOLPredecessors = BitSet(RPAREN, RBRACKET, BACKQUOTED_IDENT, THIS, SUPER, QUOTEID, STRINGLIT, NEW) final val closingParens = BitSet(RPAREN, RBRACKET, RBRACE) final val softModifierNames = Set(nme.inline, nme.opaque, nme.open, nme.transparent, nme.infix) + + def showTokenDetailed(token: Int): String = debugString(token) + + def showToken(token: Int): String = { + val str = tokenString(token) + if isKeyword(token) || token == COLONfollow || token == COLONeol then s"'$str'" else str + } } From 69c4ad53b5edc21aad86642c09fd0b3e350f53ee Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 24 May 2022 17:37:47 +0200 Subject: [PATCH 07/15] Remove side-effect in isColon Since we now convert to COLONeol only on demand, there's no need to conert back in `isColon`. --- .../dotty/tools/dotc/parsing/Parsers.scala | 37 +++++++++---------- .../dotty/tools/dotc/parsing/Scanners.scala | 11 ++---- .../src/dotty/tools/dotc/parsing/Tokens.scala | 2 +- 3 files changed, 22 insertions(+), 28 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 3eaf5ed5970a..80b3a0fcadbb 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -311,7 +311,7 @@ object Parsers { def acceptColon(): Int = val offset = in.offset - if in.isColon() then { in.nextToken(); offset } + if in.isColon then { in.nextToken(); offset } else accept(COLONop) /** semi = nl {nl} | `;' @@ -888,7 +888,7 @@ object Parsers { lookahead.nextToken() skipParams() skipParams() - lookahead.isColon() + lookahead.isColon def followingIsExtension() = val next = in.lookahead.token @@ -1458,7 +1458,7 @@ object Parsers { val paramStart = in.offset val ts = in.currentRegion.withCommasExpected { funArgType() match - case Ident(name) if name != tpnme.WILDCARD && in.isColon() => + case Ident(name) if name != tpnme.WILDCARD && in.isColon => isValParamList = true commaSeparatedRest( typedFunParam(paramStart, name.toTermName, imods), @@ -1876,7 +1876,7 @@ object Parsers { } def contextBounds(pname: TypeName): List[Tree] = - if in.isColon() then + if in.isColon then atSpan(in.skipToken()) { AppliedTypeTree(toplevelTyp(), Ident(pname)) } :: contextBounds(pname) @@ -1891,7 +1891,7 @@ object Parsers { Nil def typedOpt(): Tree = - if in.isColon() then { in.nextToken(); toplevelTyp() } + if in.isColon then { in.nextToken(); toplevelTyp() } else TypeTree().withSpan(Span(in.lastOffset)) def typeDependingOn(location: Location): Tree = @@ -2119,8 +2119,8 @@ object Parsers { else expr1Rest(postfixExpr(location), location) end expr1 - def expr1Rest(t: Tree, location: Location): Tree = in.token match - case EQUALS => + def expr1Rest(t: Tree, location: Location): Tree = + if in.token == EQUALS then t match case Ident(_) | Select(_, _) | Apply(_, _) | PrefixOp(_, _) => atSpan(startOffset(t), in.skipToken()) { @@ -2129,12 +2129,11 @@ object Parsers { } case _ => t - case COLONop | COLONfollow => + else if in.isColon then in.nextToken() ascription(t, location) - case _ => + else t - end expr1Rest def ascription(t: Tree, location: Location): Tree = atSpan(startOffset(t)) { in.token match { @@ -2363,7 +2362,7 @@ object Parsers { case _ => if isLiteral then literal() - else if in.isColon() then + else if in.isColon then syntaxError(IllegalStartSimpleExpr(tokenString(in.token))) in.nextToken() simpleExpr(location) @@ -2390,7 +2389,7 @@ object Parsers { case USCORE => atSpan(startOffset(t), in.skipToken()) { PostfixOp(t, Ident(nme.WILDCARD)) } case _ => - if in.isColon() && location == Location.InParens && followingIsLambdaParams() then + if in.isColon && location == Location.InParens && followingIsLambdaParams() then t match case id @ Ident(name) => if name.is(WildcardParamName) then @@ -2494,7 +2493,7 @@ object Parsers { !fn.isInstanceOf[Trees.Apply[?]] // allow one () as annotation argument else if lookahead.token == IDENTIFIER then lookahead.nextToken() - !lookahead.isColon() + !lookahead.isColon else in.canStartExprTokens.contains(lookahead.token) } } @@ -2727,7 +2726,7 @@ object Parsers { */ def pattern1(location: Location = Location.InPattern): Tree = val p = pattern2() - if in.token == COLONop || in.token == COLONfollow then + if in.isColon then in.nextToken() ascription(p, location) else p @@ -2928,7 +2927,7 @@ object Parsers { if allowed.contains(in.token) || in.isSoftModifier && localModifierTokens.subsetOf(allowed) // soft modifiers are admissible everywhere local modifiers are - && !in.lookahead.isColon() + && !in.lookahead.isColon then val isAccessMod = accessModifierTokens contains in.token val mods1 = addModifier(mods) @@ -3135,7 +3134,7 @@ object Parsers { val isParams = !impliedMods.is(Given) || startParamTokens.contains(in.token) - || isIdent && (in.name == nme.inline || in.lookahead.isColon()) + || isIdent && (in.name == nme.inline || in.lookahead.isColon) if isParams then commaSeparated(() => param()) else contextTypes(ofClass, nparams, impliedMods) checkVarArgsRules(clause) @@ -3748,7 +3747,7 @@ object Parsers { isUsingClause(extParams) do () leadParamss ++= paramClauses(givenOnly = true, numLeadParams = nparams) - if in.isColon() then + if in.isColon then syntaxError("no `:` expected here") in.nextToken() val methods: List[Tree] = @@ -3958,7 +3957,7 @@ object Parsers { */ def selfType(): ValDef = if (in.isIdent || in.token == THIS) - && in.lookahead.token == COLONop && followingIsSelfType() + && in.lookahead.isColon && followingIsSelfType() || in.lookahead.token == ARROW then atSpan(in.offset) { @@ -3968,7 +3967,7 @@ object Parsers { nme.WILDCARD else ident() val selfTpt = - if in.token == COLONfollow then + if in.isColon then in.nextToken() infixType() else diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 41e469eb9c10..27493029ec7f 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -75,13 +75,8 @@ object Scanners { def isNestedStart = token == LBRACE || token == INDENT def isNestedEnd = token == RBRACE || token == OUTDENT - /** Is token a COLON, after having converted COLONeol to COLON? - * The conversion means that indentation is not significant after `:` - * anymore. So, warning: this is a side-effecting operation. - */ - def isColon() = - if token == COLONeol then token = COLONop - token == COLONop || token == COLONfollow + def isColon = + token == COLONop || token == COLONfollow || token == COLONeol /** Is current token first one after a newline? */ def isAfterLineEnd: Boolean = lineOffset >= 0 @@ -1192,7 +1187,7 @@ object Scanners { isSoftModifier && inModifierPosition() def isSoftModifierInParamModifierPosition: Boolean = - isSoftModifier && lookahead.token != COLONop && lookahead.token != COLONfollow + isSoftModifier && !lookahead.isColon def isErased: Boolean = isIdent(nme.erased) && erasedEnabled diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index 9fd4759db9fc..a90374b6347d 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -283,7 +283,7 @@ object Tokens extends TokensCommon { final val endMarkerTokens = identifierTokens | BitSet(IF, WHILE, FOR, MATCH, TRY, NEW, THROW, GIVEN, VAL, THIS) - final val colonEOLPredecessors = BitSet(RPAREN, RBRACKET, BACKQUOTED_IDENT, THIS, SUPER, QUOTEID, STRINGLIT, NEW) + final val colonEOLPredecessors = BitSet(RPAREN, RBRACKET, BACKQUOTED_IDENT, THIS, SUPER, QUOTEID, STRINGLIT) final val closingParens = BitSet(RPAREN, RBRACKET, RBRACE) From 42eb08a5f1d8b9802c9213f981edccf311cb1c28 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 24 May 2022 18:15:53 +0200 Subject: [PATCH 08/15] Make sure that fewerBraces as default will not work for 3.0-migration Under 3.0-migration { x: T => expr } Is legal code but it's forbidden for later versions. If `fewerBraces` was the default, that code would be ambiguous. --- compiler/src/dotty/tools/dotc/parsing/Scanners.scala | 7 ++++++- tests/rewrites/rewrites.scala | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 27493029ec7f..eba6cb188933 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -214,7 +214,12 @@ object Scanners { if fewerBracesEnabledCtx ne myLanguageImportContext then fewerBracesEnabledCache = featureEnabled(Feature.fewerBraces) - || fewerBracesByDefault && indentSyntax + || fewerBracesByDefault && indentSyntax && !migrateTo3 + // ensure that fewer braces is not the default for 3.0-migration since + // { x: T => + // expr + // } + // would be ambiguous fewerBracesEnabledCtx = myLanguageImportContext fewerBracesEnabledCache diff --git a/tests/rewrites/rewrites.scala b/tests/rewrites/rewrites.scala index f38269bd0ed6..6e2e4c2c8d9f 100644 --- a/tests/rewrites/rewrites.scala +++ b/tests/rewrites/rewrites.scala @@ -56,7 +56,7 @@ object test3 { } } } - def g = { (x: Int) => + def g = { x: Int => x + 1 } } From d3dd7dd620bc90dbbf7203d79066d0f25585e76d Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 24 May 2022 20:28:25 +0200 Subject: [PATCH 09/15] Apply suggestions from code review Co-authored-by: Julien Richard-Foy --- docs/_docs/reference/experimental/fewer-braces.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/reference/experimental/fewer-braces.md b/docs/_docs/reference/experimental/fewer-braces.md index 7a8f5034de04..461938fff12e 100644 --- a/docs/_docs/reference/experimental/fewer-braces.md +++ b/docs/_docs/reference/experimental/fewer-braces.md @@ -56,7 +56,7 @@ The `:` can optionally be followed by the parameter part of a function literal: val xs = elems.map: x => val y = x - 1 y * y -xs.foldLeft (x, y) => +xs.foldLeft(0): (x, y) => x + y ``` Braces can be omitted if the lambda starts with a parameter list and an arrow symbol `=>` or `?=>`. From 214347473bf206481649cdaa3b9ea6301208a33e Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 25 May 2022 11:04:16 +0200 Subject: [PATCH 10/15] Rename test I have probably hit 200 times this test instead of the real Parsers.scala when trying to open it. Enough! --- tests/init/pos/{Parsers.scala => ParserLocation.scala} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/init/pos/{Parsers.scala => ParserLocation.scala} (95%) diff --git a/tests/init/pos/Parsers.scala b/tests/init/pos/ParserLocation.scala similarity index 95% rename from tests/init/pos/Parsers.scala rename to tests/init/pos/ParserLocation.scala index 2fa3f16839ba..c908fc4cb8e2 100644 --- a/tests/init/pos/Parsers.scala +++ b/tests/init/pos/ParserLocation.scala @@ -1,4 +1,4 @@ -object Parsers { +object ParserLocation { enum Location(val inParens: Boolean, val inPattern: Boolean, val inArgs: Boolean): case InParens extends Location(true, false, false) case InArgs extends Location(true, false, true) From 5b9b1a45b221e2b355176ed7fb8e9a9e739764d1 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 25 May 2022 11:05:53 +0200 Subject: [PATCH 11/15] Allow colon lambdas as operands of infix operations We already allow `: indent` there. For consistency we should allow `: params => indent` as well. --- .../dotty/tools/dotc/parsing/Parsers.scala | 22 +++++++++++++------ tests/neg/closure-args.scala | 6 ++--- tests/pos/closure-args.scala | 2 ++ 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 80b3a0fcadbb..bdf19ac7d013 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -984,6 +984,13 @@ object Parsers { recur(top) } + /** True if we are seeing a lambda argument after a colon of the form: + * : (params) => + * body + */ + def isColonLambda = + in.fewerBracesEnabled && in.token == COLONfollow && followingIsLambdaAfterColon() + /** operand { infixop operand | MatchClause } [postfixop], * * respecting rules of associativity and precedence. @@ -998,24 +1005,25 @@ object Parsers { val base = opStack def recur(top: Tree): Tree = - val isType = kind == ParseKind.Type - if (isIdent && isOperator) { + def isType = kind == ParseKind.Type + def maybePostfix = kind == ParseKind.Expr && in.postfixOpsEnabled + if isIdent && isOperator then val op = if isType then typeIdent() else termIdent() val top1 = reduceStack(base, top, precedence(op.name), !op.name.isRightAssocOperatorName, op.name, isType) opStack = OpInfo(top1, op, in.offset) :: opStack colonAtEOLOpt() newLineOptWhenFollowing(canStartOperand) - val maybePostfix = kind == ParseKind.Expr && in.postfixOpsEnabled - if (maybePostfix && !canStartOperand(in.token)) { + 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)) - } else val t = reduceStack(base, top, minPrec, leftAssoc = true, in.name, isType) if !isType && in.token == MATCH then recurAtMinPrec(matchClause(t)) @@ -2399,7 +2407,7 @@ object Parsers { makeParameter(name.asTermName, typedOpt(), Modifiers(), isBackquoted = isBackquoted(id)) } case _ => t - else if in.fewerBracesEnabled && in.token == COLONfollow && followingIsLambdaAfterColon() then + else if isColonLambda then val app = atSpan(startOffset(t), in.skipToken()) { Apply(t, expr(Location.InColonArg) :: Nil) } diff --git a/tests/neg/closure-args.scala b/tests/neg/closure-args.scala index 333cda40cfd6..def85f5924a1 100644 --- a/tests/neg/closure-args.scala +++ b/tests/neg/closure-args.scala @@ -1,10 +1,8 @@ import language.experimental.fewerBraces -val x = List().map: (x: => Int) => // error +val x = List(1).map: (x: => Int) => // error ??? -val y = List() map: x => // error - x + 1 // error -val z = List().map: + => // ok +val z = List(1).map: + => // ok ??? val xs = List(1) diff --git a/tests/pos/closure-args.scala b/tests/pos/closure-args.scala index abcf78dc2e97..98e49407f9b0 100644 --- a/tests/pos/closure-args.scala +++ b/tests/pos/closure-args.scala @@ -4,6 +4,8 @@ object Test1: val xs = List(1, 2, 3) val ys = xs.map: x => x + 1 + val ys1 = List(1) map: x => + x + 1 val x = ys.foldLeft(0): (x, y) => x + y val y = ys.foldLeft(0): (x: Int, y: Int) => From 32079fffbbaa25f133b7d78d4133b2d93aea4ab4 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 25 May 2022 11:42:03 +0200 Subject: [PATCH 12/15] Restrict set of tokens after which a colon argument is allowed Drop strings and quoted idents. That leaves: - alphanumeric identifiers, backticked identifiers, this, super, `)`, `]` This choice was made to keep the extension minimal. Quite a few files in the intent project have to be changed since they all use an idiom like "Formatting": ... with a colon after a string literal. We can discuss separately whether we want to support this, but IMO there's no pressing need to do so. --- community-build/community-projects/intent | 2 +- compiler/src/dotty/tools/dotc/parsing/Tokens.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/community-build/community-projects/intent b/community-build/community-projects/intent index 049555611f5e..466662fb36ed 160000 --- a/community-build/community-projects/intent +++ b/community-build/community-projects/intent @@ -1 +1 @@ -Subproject commit 049555611f5e9cc597943da032b7775ef179b66c +Subproject commit 466662fb36ed38d1f045449682bdc109496c6b2d diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index a90374b6347d..3d247dd683d1 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -283,7 +283,7 @@ object Tokens extends TokensCommon { final val endMarkerTokens = identifierTokens | BitSet(IF, WHILE, FOR, MATCH, TRY, NEW, THROW, GIVEN, VAL, THIS) - final val colonEOLPredecessors = BitSet(RPAREN, RBRACKET, BACKQUOTED_IDENT, THIS, SUPER, QUOTEID, STRINGLIT) + final val colonEOLPredecessors = BitSet(RPAREN, RBRACKET, BACKQUOTED_IDENT, THIS, SUPER) final val closingParens = BitSet(RPAREN, RBRACKET, RBRACE) From b1a68679cd666db9f4752daca988d71429b8d242 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 25 May 2022 22:18:52 +0200 Subject: [PATCH 13/15] Updates to documentation Drop fewerbraces.md, it's now fully part of indentation.md. --- docs/_docs/internals/syntax.md | 21 +++-- .../reference/experimental/fewer-braces.md | 76 +---------------- .../other-new-features/indentation.md | 82 ++++++++++++------- docs/sidebar.yml | 1 - 4 files changed, 66 insertions(+), 114 deletions(-) diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index 15f7c8e4d31b..0b08f85553c8 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -96,14 +96,17 @@ The lexical analyzer also inserts `indent` and `outdent` tokens that represent r In the context-free productions below we use the notation `<<< ts >>>` to indicate a token sequence `ts` that is either enclosed in a pair of braces `{ ts }` or that constitutes an indented region `indent ts outdent`. Analogously, the notation `:<<< ts >>>` indicates a token sequence `ts` that is either enclosed in a pair of braces `{ ts }` or that constitutes an indented region `indent ts outdent` that follows -a `:` at the end of a line. +a `colon` token. +A `colon` token reads as the standard colon "`:`" but is generated instead of it where `colon` is legal according to the context free syntax, but only if the previous token +is an alphanumeric identifier, a backticked identifier, or one of the tokens `this`, `super`, "`)`", and "`]`". ``` +colon ::= ':' -- with side conditions explained above <<< ts >>> ::= ‘{’ ts ‘}’ | indent ts outdent :<<< ts >>> ::= [nl] ‘{’ ts ‘}’ - | `:` indent ts outdent + | colon indent ts outdent ``` ## Keywords @@ -197,7 +200,7 @@ FunArgTypes ::= FunArgType { ‘,’ FunArgType } ParamType ::= [‘=>’] ParamValueType ParamValueType ::= Type [‘*’] PostfixOp(t, "*") TypeArgs ::= ‘[’ Types ‘]’ ts -Refinement ::= ‘{’ [RefineDcl] {semi [RefineDcl]} ‘}’ ds +Refinement ::= :<<< [RefineDcl] {semi [RefineDcl]} >>> ds TypeBounds ::= [‘>:’ Type] [‘<:’ Type] TypeBoundsTree(lo, hi) TypeParamBounds ::= TypeBounds {‘:’ Type} ContextBounds(typeBounds, tps) Types ::= Type {‘,’ Type} @@ -234,7 +237,7 @@ Catches ::= ‘catch’ (Expr | ExprCaseClause) PostfixExpr ::= InfixExpr [id] PostfixOp(expr, op) InfixExpr ::= PrefixExpr | InfixExpr id [nl] InfixExpr InfixOp(expr, op, expr) - | InfixExpr id ‘:’ IndentedExpr + | InfixExpr id ColonArgument | InfixExpr MatchClause MatchClause ::= ‘match’ <<< CaseClauses >>> Match(expr, cases) PrefixExpr ::= [PrefixOperator] SimpleExpr PrefixOp(expr, op) @@ -253,13 +256,13 @@ SimpleExpr ::= SimpleRef | SimpleExpr ‘.’ MatchClause | SimpleExpr TypeArgs TypeApply(expr, args) | SimpleExpr ArgumentExprs Apply(expr, args) - | SimpleExpr ‘:’ ColonArgument -- under language.experimental.fewerBraces + | SimpleExpr ColonArgument -- under language.experimental.fewerBraces | SimpleExpr ‘_’ PostfixOp(expr, _) (to be dropped) | XmlExpr -- to be dropped -ColonArgument ::= indent CaseClauses | Block outdent - | FunParams (‘=>’ | ‘?=>’) ColonArgBody - | HkTypeParamClause ‘=>’ ColonArgBody -ColonArgBody ::= indent (CaseClauses | Block) outdent +ColonArgument ::= colon [LambdaStart] + indent (CaseClauses | Block) outdent +LambdaStart ::= FunParams (‘=>’ | ‘?=>’) + | HkTypeParamClause ‘=>’ ExprSplice ::= spliceId -- if inside quoted block | ‘$’ ‘{’ Block ‘}’ -- unless inside quoted pattern | ‘$’ ‘{’ Pattern ‘}’ -- when inside quoted pattern diff --git a/docs/_docs/reference/experimental/fewer-braces.md b/docs/_docs/reference/experimental/fewer-braces.md index 461938fff12e..eb454886ad03 100644 --- a/docs/_docs/reference/experimental/fewer-braces.md +++ b/docs/_docs/reference/experimental/fewer-braces.md @@ -4,78 +4,4 @@ title: "Fewer Braces" nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/fewer-braces.html --- -By and large, the possible indentation regions coincide with those regions where braces `{...}` are also legal, no matter whether the braces enclose an expression or a set of definitions. There is one exception, though: Arguments to function can be enclosed in braces but they cannot be simply indented instead. Making indentation always significant for function arguments would be too restrictive and fragile. - -To allow such arguments to be written without braces, a variant of the indentation scheme is implemented under language import -```scala -import language.experimental.fewerBraces -``` -Alternatively, it can be enabled with command line option `-language:experimental.fewerBraces`. - -This variant is more contentious and less stable than the rest of the significant indentation scheme. It allows to replace a function argument in braces by a `:` at the end of a line and indented code, similar to the convention for class bodies. The `:` can -optionally be followed by the parameter part of a function literal. - -## Using `:` At End Of Line - - -Similar to what is done for classes and objects, a `:` that follows a function reference at the end of a line means braces can be omitted for function arguments. Example: -```scala -times(10): - println("ah") - println("ha") -``` - -The colon can also follow an infix operator: - -```scala -credentials ++ : - val file = Path.userHome / ".credentials" - if file.exists - then Seq(Credentials(file)) - else Seq() -``` - -Function calls that take multiple argument lists can also be handled this way: - -```scala -val firstLine = files.get(fileName).fold: - val fileNames = files.values - s"""no file named $fileName found among - |${values.mkString(\n)}""".stripMargin - : - f => - val lines = f.iterator.map(_.readLine) - lines.mkString("\n) -``` - - -## Lambda Arguments Without Braces - -The `:` can optionally be followed by the parameter part of a function literal: -```scala -val xs = elems.map: x => - val y = x - 1 - y * y -xs.foldLeft(0): (x, y) => - x + y -``` -Braces can be omitted if the lambda starts with a parameter list and an arrow symbol `=>` or `?=>`. -The arrow is followed on the next line(s) by the body of the functional literal which must be indented -relative to the previous line. - -## Syntax Changes - -As a lexical change, a `:` at the end of a line is now always treated as a -"colon at end of line" token. - -The context free grammar changes as follows: -``` -SimpleExpr ::= ... - | SimpleExpr ‘:’ ColonArgument - - | SimpleExpr FunParams (‘=>’ | ‘?=>’) IndentedArgument -ColonArgument ::= indent CaseClauses | Block outdent - | FunParams (‘=>’ | ‘?=>’) ColonArgBody - | HkTypeParamClause ‘=>’ ColonArgBody -ColonArgBody ::= indent (CaseClauses | Block) outdent -``` +The documentation contained in this file is now part of [./indentation.html]. \ No newline at end of file diff --git a/docs/_docs/reference/other-new-features/indentation.md b/docs/_docs/reference/other-new-features/indentation.md index 377869c622c9..299a01eb32e0 100644 --- a/docs/_docs/reference/other-new-features/indentation.md +++ b/docs/_docs/reference/other-new-features/indentation.md @@ -61,7 +61,7 @@ There are two rules: - after the leading parameters of an `extension`, or - after a `with` in a given instance, or - - after a ": at end of line" token (see below) + - after a `:` at the start of a template body (see discussion of `` below), or - after one of the following tokens: ``` @@ -134,12 +134,14 @@ is parsed as `if x then a + b + c else d`. The Scala grammar uses the term _template body_ for the definitions of a class, trait, or object that are normally enclosed in braces. The braces around a template body can also be omitted by means of the following rule. -If at the point where a template body can start there is a `:` that occurs at the end -of a line, and that is followed by at least one indented statement, the recognized -token is changed from ":" to ": at end of line". The latter token is one of the tokens -that can start an indentation region. The Scala grammar is changed so an optional ": at end of line" is allowed in front of a template body. +A template body can alternatively consist of a colon followed by one or more indented statements. To this purpose we introduce a new `` token that reads as +the standard colon "`:`" but is generated instead of it where `` +is legal according to the context free syntax, but only if the previous token +is an alphanumeric identifier, a backticked identifier, or one of the tokens `this`, `super`, "`)`", and "`]`". -Analogous rules apply for enum bodies and local packages containing nested definitions. +An indentation region can start after a ``. A template body may be either enclosed in braces, or it may start with +` ` and end with ``. +Analogous rules apply for enum bodies, type refinements, and local packages containing nested definitions. With these new rules, the following constructs are all valid: @@ -170,17 +172,19 @@ In each case, the `:` at the end of line can be replaced without change of meani The syntax changes allowing this are as follows: +Define for an arbitrary sequence of tokens or non-terminals `TS`: + ``` -Template ::= InheritClauses [colonEol] [TemplateBody] -EnumDef ::= id ClassConstr InheritClauses [colonEol] EnumBody -Packaging ::= ‘package’ QualId [nl | colonEol] ‘{’ TopStatSeq ‘}’ -SimpleExpr ::= ‘new’ ConstrApp {‘with’ ConstrApp} [[colonEol] TemplateBody] +:<<< TS >>> ::= ‘{’ TS ‘}’ + | +``` +Then the grammar changes as follows: +``` +TemplateBody ::= :<<< [SelfType] TemplateStat {semi TemplateStat} >>> +EnumBody ::= :<<< [SelfType] EnumStat {semi EnumStat} >>> +Refinement ::= :<<< [RefineDcl] {semi [RefineDcl]} >>> +Packaging ::= ‘package’ QualId :<<< TopStats >>> ``` - -Here, `colonEol` stands for ": at end of line", as described above. -The lexical analyzer is modified so that a `:` at the end of a line -is reported as `colonEol` if the parser is at a point where a `colonEol` is -valid as next token. ### Spaces vs Tabs @@ -444,15 +448,15 @@ indented regions where possible. When invoked with options `-rewrite -no-indent` The `-indent` option only works on [new-style syntax](./control-syntax.md). So to go from old-style syntax to new-style indented code one has to invoke the compiler twice, first with options `-rewrite -new-syntax`, then again with options `-rewrite -indent`. To go in the opposite direction, from indented code to old-style syntax, it's `-rewrite -no-indent`, followed by `-rewrite -old-syntax`. -### Variant: Indentation Marker `:` +### Variant: Indentation Marker `:` for Arguments -Generally, the possible indentation regions coincide with those regions where braces `{...}` are also legal, no matter whether the braces enclose an expression or a set of definitions. There is one exception, though: Arguments to function can be enclosed in braces but they cannot be simply indented instead. Making indentation always significant for function arguments would be too restrictive and fragile. +Generally, the possible indentation regions coincide with those regions where braces `{...}` are also legal, no matter whether the braces enclose an expression or a set of definitions. There is one exception, though: Arguments to functions can be enclosed in braces but they cannot be simply indented instead. Making indentation always significant for function arguments would be too restrictive and fragile. To allow such arguments to be written without braces, a variant of the indentation scheme is implemented under language import ```scala import language.experimental.fewerBraces ``` -This variant is more contentious and less stable than the rest of the significant indentation scheme. In this variant, a colon `:` at the end of a line is also one of the possible tokens that opens an indentation region. Examples: +In this variant, a `` token is also recognized where function argument would be expected. Examples: ```scala times(10): @@ -462,24 +466,44 @@ times(10): or +```scala +credentials `++`: + val file = Path.userHome / ".credentials" + if file.exists + then Seq(Credentials(file)) + else Seq() +``` + +or + ```scala xs.map: x => val y = x - 1 y * y ``` - -The colon is usable not only for lambdas and by-name parameters, but -also even for ordinary parameters: +What's more, a `:` in these settings can also be followed on the same line by the parameter part and arrow of a lambda. So the last example could be compressed to this: ```scala -credentials ++ : - val file = Path.userHome / ".credentials" - if file.exists - then Seq(Credentials(file)) - else Seq() +xs.map: x => + val y = x - 1 + y * y +``` +and the following would also be legal: +```scala +xs.foldLeft: (x, y) => + x + y ``` -How does this syntax variant work? Colons at the end of lines are their own token, distinct from normal `:`. -The Scala grammar is changed so that colons at end of lines are accepted at all points -where an opening brace enclosing an argument is legal. Special provisions are taken so that method result types can still use a colon on the end of a line, followed by the actual type on the next. +The grammar changes for this variant are as follows. + +``` +SimpleExpr ::= ... + | SimpleExpr ColonArgument +InfixExpr ::= ... + | InfixExpr id ColonArgument +ColonArgument ::= colon [LambdaStart] + indent (CaseClauses | Block) outdent +LambdaStart ::= FunParams (‘=>’ | ‘?=>’) + | HkTypeParamClause ‘=>’ +``` \ No newline at end of file diff --git a/docs/sidebar.yml b/docs/sidebar.yml index c7edc92c1530..963949ec9d8f 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -138,7 +138,6 @@ subsection: directory: experimental index: reference/experimental/overview.md subsection: - - page: reference/experimental/fewer-braces.md - page: reference/experimental/canthrow.md - page: reference/experimental/erased-defs.md - page: reference/experimental/erased-defs-spec.md From 293f18c00896a3103ce822a5c9aa321357368e53 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 26 May 2022 10:16:54 +0200 Subject: [PATCH 14/15] Align treatment of `colon` in template bodies and arguments --- compiler/src/dotty/tools/dotc/parsing/Scanners.scala | 6 ++++-- compiler/src/dotty/tools/dotc/parsing/Tokens.scala | 2 +- docs/_docs/internals/syntax.md | 2 +- tests/run/LazyLists.scala | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index eba6cb188933..93ce23cbc103 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -648,8 +648,10 @@ object Scanners { def observeColonEOL(inTemplate: Boolean): Unit = val enabled = - if inTemplate then token == COLONop || token == COLONfollow - else token == COLONfollow && fewerBracesEnabled + if token == COLONop && inTemplate then + report.deprecationWarning(em"`:` after symbolic operator is deprecated; use backticks around operator instead", sourcePos(offset)) + true + else token == COLONfollow && (inTemplate || fewerBracesEnabled) if enabled then lookAhead() val atEOL = isAfterLineEnd || token == EOF diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index 3d247dd683d1..82a5297faf97 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -283,7 +283,7 @@ object Tokens extends TokensCommon { final val endMarkerTokens = identifierTokens | BitSet(IF, WHILE, FOR, MATCH, TRY, NEW, THROW, GIVEN, VAL, THIS) - final val colonEOLPredecessors = BitSet(RPAREN, RBRACKET, BACKQUOTED_IDENT, THIS, SUPER) + final val colonEOLPredecessors = BitSet(RPAREN, RBRACKET, BACKQUOTED_IDENT, THIS, SUPER, NEW) final val closingParens = BitSet(RPAREN, RBRACKET, RBRACE) diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index 0b08f85553c8..ccb8f39b9b70 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -99,7 +99,7 @@ notation `:<<< ts >>>` indicates a token sequence `ts` that is either enclosed i a `colon` token. A `colon` token reads as the standard colon "`:`" but is generated instead of it where `colon` is legal according to the context free syntax, but only if the previous token -is an alphanumeric identifier, a backticked identifier, or one of the tokens `this`, `super`, "`)`", and "`]`". +is an alphanumeric identifier, a backticked identifier, or one of the tokens `this`, `super`, `new`, "`)`", and "`]`". ``` colon ::= ':' -- with side conditions explained above diff --git a/tests/run/LazyLists.scala b/tests/run/LazyLists.scala index d5d533fc64fd..2389b6de3369 100644 --- a/tests/run/LazyLists.scala +++ b/tests/run/LazyLists.scala @@ -46,7 +46,7 @@ package xcollections: val empty: LazyList[Nothing] = new: protected def force(): LazyList[Nothing] = this - object #:: : + object `#::`: def unapply[T](xs: LazyList[T]): Option[(T, LazyList[T])] = if xs.isEmpty then None else Some((xs.head, xs.tail)) From 143a0aa05f6e1065148f7b2d9b016334523f5d4c Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 26 May 2022 11:04:25 +0200 Subject: [PATCH 15/15] Align the two synyax.md versions There was quite some breakage over time where one version but not the other was edited. --- docs/_docs/internals/syntax.md | 35 +++++++++++++++++------- docs/_docs/reference/syntax.md | 50 +++++++++++++++++++++++----------- 2 files changed, 59 insertions(+), 26 deletions(-) diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index ccb8f39b9b70..35ada397a0d3 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -3,6 +3,20 @@ layout: doc-page title: "Scala 3 Syntax Summary" --- + + The following description of Scala tokens uses literal characters `‘c’` when referring to the ASCII fragment `\u0000` – `\u007F`. @@ -88,7 +102,6 @@ nl ::= “new line character” semi ::= ‘;’ | nl {nl} ``` - ## Optional Braces The lexical analyzer also inserts `indent` and `outdent` tokens that represent regions of indented code [at certain points](../reference/other-new-features/indentation.md) @@ -127,7 +140,7 @@ type val var while with yield ### Soft keywords ``` -as derives end extension infix inline opaque open transparent using | * + - +as derives end extension infix inline opaque open throws transparent using | * + - ``` See the [separate section on soft keywords](../reference/soft-modifier.md) for additional @@ -234,13 +247,13 @@ Expr1 ::= [‘inline’] ‘if’ ‘(’ Expr ‘)’ {nl} Expr [[ Ascription ::= ‘:’ InfixType Typed(expr, tp) | ‘:’ Annotation {Annotation} Typed(expr, Annotated(EmptyTree, annot)*) Catches ::= ‘catch’ (Expr | ExprCaseClause) -PostfixExpr ::= InfixExpr [id] PostfixOp(expr, op) +PostfixExpr ::= InfixExpr [id] PostfixOp(expr, op) -- only if language.postfixOperators is enabled InfixExpr ::= PrefixExpr | InfixExpr id [nl] InfixExpr InfixOp(expr, op, expr) | InfixExpr id ColonArgument | InfixExpr MatchClause MatchClause ::= ‘match’ <<< CaseClauses >>> Match(expr, cases) -PrefixExpr ::= [PrefixOperator] SimpleExpr PrefixOp(expr, op) +PrefixExpr ::= [PrefixOperator] SimpleExpr PrefixOp(expr, op) PrefixOperator ::= ‘-’ | ‘+’ | ‘~’ | ‘!’ SimpleExpr ::= SimpleRef | Literal @@ -258,11 +271,13 @@ SimpleExpr ::= SimpleRef | SimpleExpr ArgumentExprs Apply(expr, args) | SimpleExpr ColonArgument -- under language.experimental.fewerBraces | SimpleExpr ‘_’ PostfixOp(expr, _) (to be dropped) - | XmlExpr -- to be dropped -ColonArgument ::= colon [LambdaStart] - indent (CaseClauses | Block) outdent -LambdaStart ::= FunParams (‘=>’ | ‘?=>’) - | HkTypeParamClause ‘=>’ + | XmlExpr -- to be dropped +ColonArgument ::= colon [LambdaStart] + indent (CaseClauses | Block) outdent +LambdaStart ::= FunParams (‘=>’ | ‘?=>’) + | HkTypeParamClause ‘=>’ +Quoted ::= ‘'’ ‘{’ Block ‘}’ + | ‘'’ ‘[’ Type ‘]’ ExprSplice ::= spliceId -- if inside quoted block | ‘$’ ‘{’ Block ‘}’ -- unless inside quoted pattern | ‘$’ ‘{’ Pattern ‘}’ -- when inside quoted pattern @@ -303,7 +318,7 @@ TypeCaseClause ::= ‘case’ (InfixType | ‘_’) ‘=>’ Type [semi] Pattern ::= Pattern1 { ‘|’ Pattern1 } Alternative(pats) Pattern1 ::= Pattern2 [‘:’ RefinedType] Bind(name, Typed(Ident(wildcard), tpe)) -Pattern2 ::= [id ‘@’] InfixPattern Bind(name, pat) +Pattern2 ::= [id ‘@’] InfixPattern [‘*’] Bind(name, pat) InfixPattern ::= SimplePattern { id [nl] SimplePattern } InfixOp(pat, op, pat) SimplePattern ::= PatVar Ident(wildcard) | Literal Bind(name, Ident(wildcard)) diff --git a/docs/_docs/reference/syntax.md b/docs/_docs/reference/syntax.md index cbc570bb4e30..c26354bd6a25 100644 --- a/docs/_docs/reference/syntax.md +++ b/docs/_docs/reference/syntax.md @@ -4,6 +4,20 @@ title: "Scala 3 Syntax Summary" nightlyOf: https://docs.scala-lang.org/scala3/reference/syntax.html --- + + The following description of Scala tokens uses literal characters `‘c’` when referring to the ASCII fragment `\u0000` – `\u007F`. @@ -28,12 +42,12 @@ upper ::= ‘A’ | … | ‘Z’ | ‘\$’ | ‘_’ “… and U lower ::= ‘a’ | … | ‘z’ “… and Unicode category Ll” letter ::= upper | lower “… and Unicode categories Lo, Lt, Nl” digit ::= ‘0’ | … | ‘9’ -paren ::= ‘(’ | ‘)’ | ‘[’ | ‘]’ | ‘{’ | ‘}’ | ‘'(’ | ‘'[’ | ‘'{’ +paren ::= ‘(’ | ‘)’ | ‘[’ | ‘]’ | ‘{’ | ‘}’ delim ::= ‘`’ | ‘'’ | ‘"’ | ‘.’ | ‘;’ | ‘,’ -opchar ::= “printableChar not matched by (whiteSpace | upper | - lower | letter | digit | paren | delim | opchar | - Unicode_Sm | Unicode_So)” -printableChar ::= “all characters in [\u0020, \u007F] inclusive” +opchar ::= ‘!’ | ‘#’ | ‘%’ | ‘&’ | ‘*’ | ‘+’ | ‘-’ | ‘/’ | ‘:’ | + ‘<’ | ‘=’ | ‘>’ | ‘?’ | ‘@’ | ‘\’ | ‘^’ | ‘|’ | ‘~’ + “… and Unicode categories Sm, So” +printableChar ::= “all characters in [\u0020, \u007E] inclusive” charEscapeSeq ::= ‘\’ (‘b’ | ‘t’ | ‘n’ | ‘f’ | ‘r’ | ‘"’ | ‘'’ | ‘\’) op ::= opchar {opchar} @@ -46,6 +60,7 @@ id ::= plainid | ‘`’ { charNoBackQuoteOrNewline | UnicodeEscape | charEscapeSeq } ‘`’ idrest ::= {letter | digit} [‘_’ op] quoteId ::= ‘'’ alphaid +spliceId ::= ‘$’ alphaid ; integerLiteral ::= (decimalNumeral | hexNumeral) [‘L’ | ‘l’] decimalNumeral ::= ‘0’ | nonZeroDigit [{digit | ‘_’} digit] @@ -95,13 +110,17 @@ The lexical analyzer also inserts `indent` and `outdent` tokens that represent r In the context-free productions below we use the notation `<<< ts >>>` to indicate a token sequence `ts` that is either enclosed in a pair of braces `{ ts }` or that constitutes an indented region `indent ts outdent`. Analogously, the notation `:<<< ts >>>` indicates a token sequence `ts` that is either enclosed in a pair of braces `{ ts }` or that constitutes an indented region `indent ts outdent` that follows -a `:` at the end of a line. +a `colon` token. + +A `colon` token reads as the standard colon "`:`" but is generated instead of it where `colon` is legal according to the context free syntax, but only if the previous token +is an alphanumeric identifier, a backticked identifier, or one of the tokens `this`, `super`, `new`, "`)`", and "`]`". ``` +colon ::= ':' -- with side conditions explained above <<< ts >>> ::= ‘{’ ts ‘}’ | indent ts outdent :<<< ts >>> ::= [nl] ‘{’ ts ‘}’ - | `:` indent ts outdent + | colon indent ts outdent ``` ## Keywords @@ -115,15 +134,14 @@ given if implicit import lazy match new null object override package private protected return sealed super then throw trait true try type val var while with yield -: = <- => <: :> # +: = <- => <: >: # @ =>> ?=> ``` ### Soft keywords ``` -as derives end extension infix inline opaque open throws -transparent using | * + - +as derives end extension infix inline opaque open transparent using | * + - ``` See the [separate section on soft keywords](./soft-modifier.md) for additional @@ -182,8 +200,6 @@ SimpleType ::= SimpleLiteral | Singleton ‘.’ ‘type’ | ‘(’ Types ‘)’ | Refinement - | ‘$’ ‘{’ Block ‘}’ -- unless inside quoted pattern - | ‘$’ ‘{’ Pattern ‘}’ -- only inside quoted pattern | SimpleType1 TypeArgs | SimpleType1 ‘#’ id Singleton ::= SimpleRef @@ -196,7 +212,7 @@ FunArgTypes ::= FunArgType { ‘,’ FunArgType } ParamType ::= [‘=>’] ParamValueType ParamValueType ::= Type [‘*’] TypeArgs ::= ‘[’ Types ‘]’ -Refinement ::= ‘{’ [RefineDcl] {semi [RefineDcl]} ‘}’ +Refinement ::= :<<< [RefineDcl] {semi [RefineDcl]} >>> TypeBounds ::= [‘>:’ Type] [‘<:’ Type] TypeParamBounds ::= TypeBounds {‘:’ Type} Types ::= Type {‘,’ Type} @@ -241,8 +257,7 @@ SimpleExpr ::= SimpleRef | Literal | ‘_’ | BlockExpr - | ‘$’ ‘{’ Block ‘}’ -- unless inside quoted pattern - | ‘$’ ‘{’ Pattern ‘}’ -- only inside quoted pattern + | ExprSplice | Quoted | quoteId -- only inside splices | ‘new’ ConstrApp {‘with’ ConstrApp} [TemplateBody] @@ -254,6 +269,9 @@ SimpleExpr ::= SimpleRef | SimpleExpr ArgumentExprs Quoted ::= ‘'’ ‘{’ Block ‘}’ | ‘'’ ‘[’ Type ‘]’ +ExprSplice ::= spliceId -- if inside quoted block + | ‘$’ ‘{’ Block ‘}’ -- unless inside quoted pattern + | ‘$’ ‘{’ Pattern ‘}’ -- when inside quoted pattern ExprsInParens ::= ExprInParens {‘,’ ExprInParens} ExprInParens ::= PostfixExpr ‘:’ Type | Expr @@ -284,7 +302,7 @@ CaseClauses ::= CaseClause { CaseClause } CaseClause ::= ‘case’ Pattern [Guard] ‘=>’ Block ExprCaseClause ::= ‘case’ Pattern [Guard] ‘=>’ Expr TypeCaseClauses ::= TypeCaseClause { TypeCaseClause } -TypeCaseClause ::= ‘case’ InfixType ‘=>’ Type [semi] +TypeCaseClause ::= ‘case’ (InfixType | ‘_’) ‘=>’ Type [semi] Pattern ::= Pattern1 { ‘|’ Pattern1 } Pattern1 ::= Pattern2 [‘:’ RefinedType]