From 1d07c1155e7a99a00200a20757fb2940ce2068f3 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 15 Mar 2021 17:13:37 +0100 Subject: [PATCH 01/14] Accept more `:` at EOLs under -Yindent-colon Systamtically accept a `:` as COLON when it cannot be confused with a `:` at EOL. --- .../dotty/tools/dotc/parsing/Parsers.scala | 56 ++++++++----------- .../dotty/tools/dotc/parsing/Scanners.scala | 8 +++ 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 16a044a759eb..ba5938c2c1f9 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -309,6 +309,11 @@ object Parsers { offset } + def acceptColon(): Int = + val offset = in.offset + if in.isColon() then { in.nextToken(); offset } + else accept(COLON) + /** semi = nl {nl} | `;' * nl = `\n' // where allowed */ @@ -861,7 +866,7 @@ object Parsers { lookahead.nextToken() skipParams() skipParams() - lookahead.token == COLON + lookahead.isColon() def followingIsExtension() = val next = in.lookahead.token @@ -1378,7 +1383,7 @@ object Parsers { if isErased then imods = addModifier(imods) val paramStart = in.offset val ts = funArgType() match { - case Ident(name) if name != tpnme.WILDCARD && in.token == COLON => + case Ident(name) if name != tpnme.WILDCARD && in.isColon() => isValParamList = true funArgTypesRest( typedFunParam(paramStart, name.toTermName, imods), @@ -1467,7 +1472,7 @@ object Parsers { /** TypedFunParam ::= id ':' Type */ def typedFunParam(start: Offset, name: TermName, mods: Modifiers = EmptyModifiers): ValDef = atSpan(start) { - accept(COLON) + acceptColon() makeParameter(name, typ(), mods) } @@ -1761,24 +1766,23 @@ object Parsers { else atSpan((t.span union cbs.head.span).start) { ContextBounds(t, cbs) } } - def contextBounds(pname: TypeName): List[Tree] = in.token match { - case COLON => + def contextBounds(pname: TypeName): List[Tree] = + if in.isColon() then atSpan(in.skipToken()) { AppliedTypeTree(toplevelTyp(), Ident(pname)) } :: contextBounds(pname) - case VIEWBOUND => + else if in.token == VIEWBOUND then report.errorOrMigrationWarning( "view bounds `<%' are no longer supported, use a context bound `:' instead", in.sourcePos()) atSpan(in.skipToken()) { Function(Ident(pname) :: Nil, toplevelTyp()) } :: contextBounds(pname) - case _ => + else Nil - } def typedOpt(): Tree = - if (in.token == COLON) { in.nextToken(); toplevelTyp() } + if in.isColon() then { in.nextToken(); toplevelTyp() } else TypeTree().withSpan(Span(in.lastOffset)) def typeDependingOn(location: Location): Tree = @@ -2195,6 +2199,7 @@ object Parsers { * | SimpleExpr `.` MatchClause * | SimpleExpr (TypeArgs | NamedTypeArgs) * | SimpleExpr1 ArgumentExprs + * | SimpleExpr1 `:` nl ArgumentExprs * Quoted ::= ‘'’ ‘{’ Block ‘}’ * | ‘'’ ‘[’ Type ‘]’ */ @@ -2342,10 +2347,9 @@ object Parsers { lookahead.nextToken() if (lookahead.token == RPAREN) !fn.isInstanceOf[Trees.Apply[?]] // allow one () as annotation argument - else if (lookahead.token == IDENTIFIER) { + else if lookahead.token == IDENTIFIER then lookahead.nextToken() - lookahead.token != COLON - } + !lookahead.isColon() else in.canStartExprTokens.contains(lookahead.token) } } @@ -2762,7 +2766,7 @@ object Parsers { if allowed.contains(in.token) || in.isSoftModifier && localModifierTokens.subsetOf(allowed) // soft modifiers are admissible everywhere local modifiers are - && in.lookahead.token != COLON + && !in.lookahead.isColon() then val isAccessMod = accessModifierTokens contains in.token val mods1 = addModifier(mods) @@ -2931,7 +2935,7 @@ object Parsers { } atSpan(start, nameStart) { val name = ident() - accept(COLON) + acceptColon() if (in.token == ARROW && ofClass && !mods.is(Local)) syntaxError(VarValParametersMayNotBeCallByName(name, mods.is(Mutable))) val tpt = paramType() @@ -2969,7 +2973,7 @@ object Parsers { val isParams = !impliedMods.is(Given) || startParamTokens.contains(in.token) - || isIdent && (in.name == nme.inline || in.lookahead.token == COLON) + || isIdent && (in.name == nme.inline || in.lookahead.isColon()) if isParams then commaSeparated(() => param()) else contextTypes(ofClass, nparams) checkVarArgsRules(clause) @@ -3191,13 +3195,7 @@ object Parsers { case _ => first :: Nil } - def emptyType = TypeTree().withSpan(Span(in.lastOffset)) - val tpt = - if (in.token == COLON) { - in.nextToken() - toplevelTyp() - } - else emptyType + val tpt = typedOpt() val rhs = if tpt.isEmpty || in.token == EQUALS then accept(EQUALS) @@ -3276,15 +3274,7 @@ object Parsers { var name = ident.name.asTermName val tparams = typeParamClauseOpt(ParamOwner.Def) val vparamss = paramClauses(numLeadParams = numLeadParams) - var tpt = fromWithinReturnType { - if in.token == COLONEOL then in.token = COLON - // a hack to allow - // - // def f(): - // T - // - typedOpt() - } + var tpt = fromWithinReturnType { typedOpt() } if (migrateTo3) newLineOptWhenFollowedBy(LBRACE) val rhs = if in.token == EQUALS then @@ -3528,7 +3518,7 @@ object Parsers { else Nil newLinesOpt() val noParams = tparams.isEmpty && vparamss.isEmpty - if !(name.isEmpty && noParams) then accept(COLON) + if !(name.isEmpty && noParams) then acceptColon() val parents = if isSimpleLiteral then rejectWildcardType(annotType()) :: Nil else constrApp() :: withConstrApps() @@ -3570,7 +3560,7 @@ object Parsers { isUsingClause(extParams) do () leadParamss ++= paramClauses(givenOnly = true, numLeadParams = nparams) - if in.token == COLON then + if in.isColon() then syntaxError("no `:` expected here") in.nextToken() val methods = diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 789a76e88190..37e5535f70f6 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -76,6 +76,14 @@ 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 = COLON + token == COLON + /** Is current token first one after a newline? */ def isAfterLineEnd: Boolean = lineOffset >= 0 From 20d2e2a83b2b64a85ba72d07d7762a6b73c0eda6 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 15 Mar 2021 17:48:42 +0100 Subject: [PATCH 02/14] Disallow `:` after return Treat instead indentation as significant. I.e. return 2 // one statement return println(2) // two statements --- .../src/dotty/tools/dotc/parsing/Parsers.scala | 5 ----- .../src/dotty/tools/dotc/parsing/Scanners.scala | 6 +++--- .../src/dotty/tools/dotc/parsing/Tokens.scala | 2 +- tests/run/returns.scala | 16 ++++++++++++++++ 4 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 tests/run/returns.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index ba5938c2c1f9..eb1d4532b585 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1971,7 +1971,6 @@ object Parsers { atSpan(in.skipToken()) { Throw(expr()) } case RETURN => atSpan(in.skipToken()) { - colonAtEOLOpt() Return(if (isExprIntro) expr() else EmptyTree, EmptyTree) } case FOR => @@ -2240,10 +2239,6 @@ object Parsers { case MACRO => val start = in.skipToken() MacroTree(simpleExpr()) - case COLONEOL => - syntaxError("':' not allowed here") - in.nextToken() - simpleExpr() case _ => if (isLiteral) literal() else { diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 37e5535f70f6..2f9103aebd33 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -401,8 +401,8 @@ object Scanners { true } - def isContinuingParens() = - openParensTokens.contains(token) + def isContinuing(lastToken: Token) = + (openParensTokens.contains(token) || lastToken == RETURN) && !pastBlankLine && !migrateTo3 && !noindentSyntax @@ -504,7 +504,7 @@ object Scanners { && canEndStatTokens.contains(lastToken) && canStartStatTokens.contains(token) && !isLeadingInfixOperator() - && !(lastWidth < nextWidth && isContinuingParens()) + && !(lastWidth < nextWidth && isContinuing(lastToken)) then insert(if (pastBlankLine) NEWLINES else NEWLINE, lineOffset) else if indentIsSignificant then diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index 24a38f9d5e0a..27f533408deb 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -273,7 +273,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) + 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. diff --git a/tests/run/returns.scala b/tests/run/returns.scala new file mode 100644 index 000000000000..e329035f5a79 --- /dev/null +++ b/tests/run/returns.scala @@ -0,0 +1,16 @@ +def foo(x: Int): Int = + if x == 1 then + return + x + else + 2 + +def bar(): Unit = + return + assert(false) + +@main def Test = + assert(foo(1) == 1) + bar() + + From cd01d256c7269f108406fc2d7d124ea1925c2c5e Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 18 Mar 2021 12:07:16 +0100 Subject: [PATCH 03/14] Replace -Yindent-colons with language import Replace -Yindent-colons with language import import language.experimental.fewerBraces --- compiler/src/dotty/tools/dotc/config/Feature.scala | 3 ++- .../dotty/tools/dotc/config/ScalaSettings.scala | 2 +- .../src/dotty/tools/dotc/parsing/Parsers.scala | 2 +- .../src/dotty/tools/dotc/parsing/Scanners.scala | 14 ++++++++++---- .../test/dotty/tools/dotc/CompilationTests.scala | 1 - .../reference/other-new-features/indentation.md | 7 +++++-- .../src/scala/runtime/stdLibPatches/language.scala | 3 +++ tests/{pos-special => pos}/indent-colons.scala | 1 + 8 files changed, 23 insertions(+), 10 deletions(-) rename tests/{pos-special => pos}/indent-colons.scala (97%) diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 42fc2d0e9375..182dd4aed248 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -25,7 +25,8 @@ object Feature: val dependent = experimental("dependent") val erasedDefinitions = experimental("erasedDefinitions") - val symbolLiterals: TermName = deprecated("symbolLiterals") + val symbolLiterals = deprecated("symbolLiterals") + val fewerBraces = experimental("fewerBraces") /** Is `feature` enabled by by a command-line setting? The enabling setting is * diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 01e7d87117d2..55071f539823 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -121,7 +121,7 @@ class ScalaSettings extends Settings.SettingGroup with CommonScalaSettings { val oldSyntax: Setting[Boolean] = BooleanSetting("-old-syntax", "Require `(...)` around conditions.") val indent: Setting[Boolean] = BooleanSetting("-indent", "Together with -rewrite, remove {...} syntax when possible due to significant indentation.") val noindent: Setting[Boolean] = BooleanSetting("-no-indent", "Require classical {...} syntax, indentation is not significant.", aliases = List("-noindent")) - val YindentColons: Setting[Boolean] = BooleanSetting("-Yindent-colons", "Allow colons at ends-of-lines to start indentation blocks.") + val YindentColons: Setting[Boolean] = BooleanSetting("-Yindent-colons", "(disabled: use -language:experimental.fewerBraces instead)") /** Decompiler settings */ val printTasty: Setting[Boolean] = BooleanSetting("-print-tasty", "Prints the raw tasty.", aliases = List("--print-tasty")) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index eb1d4532b585..4a0451ea9fed 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -743,7 +743,7 @@ object Parsers { } }) canRewrite &= (in.isAfterLineEnd || statCtdTokens.contains(in.token)) // test (5) - if (canRewrite && (!underColonSyntax || in.colonSyntax)) { + if (canRewrite && (!underColonSyntax || in.fewerBracesEnabled)) { val openingPatchStr = if !colonRequired then "" else if testChar(startOpening - 1, Chars.isOperatorPart(_)) then " :" diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 2f9103aebd33..bdd08742bf8a 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -182,9 +182,6 @@ object Scanners { ((if (Config.defaultIndent) !noindentSyntax else ctx.settings.indent.value) || rewriteNoIndent) && !isInstanceOf[LookaheadScanner] - val colonSyntax = - ctx.settings.YindentColons.value - || rewriteNoIndent if (rewrite) { val s = ctx.settings @@ -201,6 +198,15 @@ object Scanners { def featureEnabled(name: TermName) = Feature.enabled(name)(using languageImportContext) def erasedEnabled = featureEnabled(Feature.erasedDefinitions) || ctx.settings.YerasedTerms.value + private var fewerBracesEnabledCache = false + private var fewerBracesEnabledCtx: Context = NoContext + + def fewerBracesEnabled = + if fewerBracesEnabledCtx ne myLanguageImportContext then + fewerBracesEnabledCache = featureEnabled(Feature.fewerBraces) + fewerBracesEnabledCtx = myLanguageImportContext + fewerBracesEnabledCache + /** All doc comments kept by their end position in a `Map`. * * Note: the map is necessary since the comments are looked up after an @@ -635,7 +641,7 @@ object Scanners { else reset() case COLON => - if colonSyntax then observeColonEOL() + if fewerBracesEnabled then observeColonEOL() case RBRACE | RPAREN | RBRACKET => closeIndented() case EOF => diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 16ee334bc81c..d034bd21ca34 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -49,7 +49,6 @@ class CompilationTests { defaultOptions.and("-nowarn", "-Xfatal-warnings") ), compileFile("tests/pos-special/typeclass-scaling.scala", defaultOptions.and("-Xmax-inlines", "40")), - compileFile("tests/pos-special/indent-colons.scala", defaultOptions.and("-Yindent-colons")), compileFile("tests/pos-special/i7296.scala", defaultOptions.and("-source", "future", "-deprecation", "-Xfatal-warnings")), compileFile("tests/pos-special/notNull.scala", defaultOptions.and("-Yexplicit-nulls")), compileDir("tests/pos-special/adhoc-extension", defaultOptions.and("-source", "future", "-feature", "-Xfatal-warnings")), diff --git a/docs/docs/reference/other-new-features/indentation.md b/docs/docs/reference/other-new-features/indentation.md index 76ed1dd3680b..7d28c821cbf3 100644 --- a/docs/docs/reference/other-new-features/indentation.md +++ b/docs/docs/reference/other-new-features/indentation.md @@ -391,8 +391,11 @@ The `-indent` option only works on [new-style syntax](./control-syntax.md). So t 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. -To allow such arguments to be written without braces, a variant of the indentation scheme is implemented under -option `-Yindent-colons`. 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: +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: ```scala times(10): diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index 08fef20e3339..e46c3701c78a 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -43,6 +43,9 @@ object language: */ object erasedDefinitions + /** Experimental support for using indentation for arguments + */ + object fewerBraces end experimental /** The deprecated object contains features that are no longer officially suypported in Scala. diff --git a/tests/pos-special/indent-colons.scala b/tests/pos/indent-colons.scala similarity index 97% rename from tests/pos-special/indent-colons.scala rename to tests/pos/indent-colons.scala index 44fe09b39cd4..aa611e796fba 100644 --- a/tests/pos-special/indent-colons.scala +++ b/tests/pos/indent-colons.scala @@ -1,3 +1,4 @@ +import language.experimental.fewerBraces object Test: locally: From e80a1929ab3c6cb78cddb1359237668eee0ede2d Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 18 Mar 2021 12:11:46 +0100 Subject: [PATCH 04/14] Drop -Yindent-colons in CB --- community-build/community-projects/intent | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/community-build/community-projects/intent b/community-build/community-projects/intent index 38c7314fb764..b1a62511741d 160000 --- a/community-build/community-projects/intent +++ b/community-build/community-projects/intent @@ -1 +1 @@ -Subproject commit 38c7314fb7643bc1786e4358b83a815194071d48 +Subproject commit b1a62511741d84cc84fdf14c051804fe81629cdb From 53928a9ac4925d8547e223c0acdc39483ae9e937 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 18 Mar 2021 13:12:55 +0100 Subject: [PATCH 05/14] Use isArrow utility method in Parser --- .../src/dotty/tools/dotc/parsing/Parsers.scala | 16 +++++++--------- .../src/dotty/tools/dotc/parsing/Scanners.scala | 3 +++ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 4a0451ea9fed..a2fd81fd6a2e 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -198,12 +198,11 @@ object Parsers { in.token match { case USCORE => true case IDENTIFIER | BACKQUOTED_IDENT => - val nxt = in.lookahead.token - nxt == ARROW || nxt == CTXARROW + in.lookahead.isArrow case LPAREN => val lookahead = in.LookaheadScanner() lookahead.skipParens() - lookahead.token == ARROW || lookahead.token == CTXARROW + lookahead.isArrow case _ => false } } && !in.isSoftModifierInModifierPosition @@ -1392,7 +1391,7 @@ object Parsers { funArgTypesRest(t, funArgType) } accept(RPAREN) - if isValParamList || in.token == ARROW || in.token == CTXARROW then + if isValParamList || in.isArrow then functionRest(ts) else { val ts1 = @@ -1881,15 +1880,13 @@ object Parsers { finally placeholderParams = saved val t = expr1(location) - if (in.token == ARROW || in.token == CTXARROW) { + if in.isArrow then placeholderParams = Nil // don't interpret `_' to the left of `=>` as placeholder val paramMods = if in.token == CTXARROW then Modifiers(Given) else EmptyModifiers wrapPlaceholders(closureRest(start, location, convertToParams(t, paramMods))) - } - else if (isWildcard(t)) { + else if isWildcard(t) then placeholderParams = placeholderParams ::: saved t - } else wrapPlaceholders(t) } } @@ -2262,7 +2259,8 @@ object Parsers { val app = atSpan(startOffset(t), in.offset) { mkApply(t, argumentExprs()) } simpleExprRest(app, canApply = true) case USCORE => - atSpan(startOffset(t), in.skipToken()) { PostfixOp(t, Ident(nme.WILDCARD)) } + if in.lookahead.isArrow then ??? + else atSpan(startOffset(t), in.skipToken()) { PostfixOp(t, Ident(nme.WILDCARD)) } case _ => t } diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index bdd08742bf8a..20a295c690c8 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -89,6 +89,9 @@ object Scanners { def isOperator = token == IDENTIFIER && isOperatorPart(name(name.length - 1)) + + def isArrow = + token == ARROW || token == CTXARROW } abstract class ScannerCommon(source: SourceFile)(using Context) extends CharArrayReader with TokenData { From 2093b939e599b6c128609f974096a45033026e78 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 18 Mar 2021 13:17:26 +0100 Subject: [PATCH 06/14] Generalize TokenData#isOperator --- compiler/src/dotty/tools/dotc/parsing/Scanners.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 20a295c690c8..3ed3a8b980a1 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -88,7 +88,8 @@ object Scanners { def isAfterLineEnd: Boolean = lineOffset >= 0 def isOperator = - token == IDENTIFIER && isOperatorPart(name(name.length - 1)) + token == BACKQUOTED_IDENT + || token == IDENTIFIER && isOperatorPart(name(name.length - 1)) def isArrow = token == ARROW || token == CTXARROW @@ -370,8 +371,7 @@ object Scanners { */ def isLeadingInfixOperator(inConditional: Boolean = true) = allowLeadingInfixOperators - && ( token == BACKQUOTED_IDENT - || token == IDENTIFIER && isOperatorPart(name(name.length - 1))) + && isOperator && (isWhitespace(ch) || ch == LF) && !pastBlankLine && { @@ -389,7 +389,7 @@ object Scanners { // leading infix operator. def assumeStartsExpr(lexeme: TokenData) = canStartExprTokens.contains(lexeme.token) - && (token != BACKQUOTED_IDENT || !lexeme.isOperator || nme.raw.isUnary(lexeme.name)) + && (!lexeme.isOperator || nme.raw.isUnary(lexeme.name)) val lookahead = LookaheadScanner() lookahead.allowLeadingInfixOperators = false // force a NEWLINE a after current token if it is on its own line From dbfde1c9e485366b8014c5f74a88937b91743fb0 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 18 Mar 2021 17:09:07 +0100 Subject: [PATCH 07/14] Add closure arguments, first version --- .../dotty/tools/dotc/parsing/Parsers.scala | 130 ++++++++++++------ docs/docs/internals/syntax.md | 4 +- tests/neg/closure-args.scala | 7 + tests/neg/i1779.scala | 4 +- tests/neg/i6059.scala | 4 +- tests/neg/i7751.scala | 2 +- tests/pos/closure-args.scala | 14 ++ 7 files changed, 114 insertions(+), 51 deletions(-) create mode 100644 tests/neg/closure-args.scala create mode 100644 tests/pos/closure-args.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index a2fd81fd6a2e..78a8619ab179 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 { case InParens extends Location(true, false, false) case InArgs extends Location(true, 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 case InBlock extends Location(false, false, false) case ElseWhere extends Location(false, false, false) @@ -403,22 +404,23 @@ object Parsers { /** Convert tree to formal parameter list */ - def convertToParams(tree: Tree, mods: Modifiers): List[ValDef] = tree match { - case Parens(t) => - convertToParam(t, mods) :: Nil - case Tuple(ts) => - ts.map(convertToParam(_, mods)) - case t: Typed => - report.errorOrMigrationWarning( - em"parentheses are required around the parameter of a lambda${rewriteNotice()}", - in.sourcePos()) - if migrateTo3 then - patch(source, t.span.startPos, "(") - patch(source, t.span.endPos, ")") - convertToParam(t, mods) :: Nil - case t => - convertToParam(t, mods) :: Nil - } + def convertToParams(tree: Tree): List[ValDef] = + val mods = if in.token == CTXARROW then Modifiers(Given) else EmptyModifiers + tree match + case Parens(t) => + convertToParam(t, mods) :: Nil + case Tuple(ts) => + ts.map(convertToParam(_, mods)) + case t: Typed => + report.errorOrMigrationWarning( + em"parentheses are required around the parameter of a lambda${rewriteNotice()}", + in.sourcePos()) + if migrateTo3 then + patch(source, t.span.startPos, "(") + patch(source, t.span.endPos, ")") + convertToParam(t, mods) :: Nil + case t => + convertToParam(t, mods) :: Nil /** Convert tree to formal parameter */ @@ -920,7 +922,8 @@ object Parsers { * @param maybePostfix postfix operators are allowed. */ def infixOps( - first: Tree, canStartOperand: Token => Boolean, operand: () => Tree, + first: Tree, canStartOperand: Token => Boolean, operand: Location => Tree, + location: Location, isType: Boolean, isOperator: => Boolean, maybePostfix: Boolean = false): Tree = { @@ -941,7 +944,7 @@ object Parsers { PostfixOp(od, topInfo.operator) } } - else recur(operand()) + else recur(operand(location)) } else val t = reduceStack(base, top, minPrec, leftAssoc = true, in.name, isType) @@ -1488,12 +1491,15 @@ object Parsers { def infixType(): Tree = infixTypeRest(refinedType()) def infixTypeRest(t: Tree): Tree = - infixOps(t, canStartTypeTokens, refinedType, isType = true, + infixOps(t, canStartTypeTokens, refinedTypeFn, Location.ElseWhere, + isType = true, isOperator = !followingIsVararg()) /** RefinedType ::= WithType {[nl] Refinement} */ - val refinedType: () => Tree = () => refinedTypeRest(withType()) + val refinedTypeFn: Location => Tree = _ => refinedType() + + def refinedType() = refinedTypeRest(withType()) def refinedTypeRest(t: Tree): Tree = { argumentStart() @@ -1811,7 +1817,11 @@ object Parsers { if in.token != altToken then if toBeContinued(altToken) then t = inSepRegion(InCond) { - expr1Rest(postfixExprRest(simpleExprRest(t)), Location.ElseWhere) + expr1Rest( + postfixExprRest( + simpleExprRest(t, Location.ElseWhere), + Location.ElseWhere), + Location.ElseWhere) } else if rewriteToNewSyntax(t.span) then @@ -1882,8 +1892,7 @@ object Parsers { val t = expr1(location) if in.isArrow then placeholderParams = Nil // don't interpret `_' to the left of `=>` as placeholder - val paramMods = if in.token == CTXARROW then Modifiers(Given) else EmptyModifiers - wrapPlaceholders(closureRest(start, location, convertToParams(t, paramMods))) + wrapPlaceholders(closureRest(start, location, convertToParams(t))) else if isWildcard(t) then placeholderParams = placeholderParams ::: saved t @@ -2155,30 +2164,30 @@ object Parsers { * | InfixExpr MatchClause */ def postfixExpr(location: Location = Location.ElseWhere): Tree = - val t = postfixExprRest(prefixExpr(), location) + val t = postfixExprRest(prefixExpr(location), location) if location.inArgs && followingIsVararg() then Typed(t, atSpan(in.skipToken()) { Ident(tpnme.WILDCARD_STAR) }) else t - def postfixExprRest(t: Tree, location: Location = Location.ElseWhere): Tree = - infixOps(t, in.canStartExprTokens, prefixExpr, + def postfixExprRest(t: Tree, location: Location): Tree = + infixOps(t, in.canStartExprTokens, prefixExpr, location, isType = false, isOperator = !(location.inArgs && followingIsVararg()), maybePostfix = true) /** PrefixExpr ::= [`-' | `+' | `~' | `!'] SimpleExpr */ - val prefixExpr: () => Tree = () => + val prefixExpr: Location => Tree = location => if (isIdent && nme.raw.isUnary(in.name)) { val start = in.offset val op = termIdent() if (op.name == nme.raw.MINUS && isNumericLit) - simpleExprRest(literal(start), canApply = true) + simpleExprRest(literal(start), location, canApply = true) else - atSpan(start) { PrefixOp(op, simpleExpr()) } + atSpan(start) { PrefixOp(op, simpleExpr(location)) } } - else simpleExpr() + else simpleExpr(location) /** SimpleExpr ::= ‘new’ ConstrApp {`with` ConstrApp} [TemplateBody] * | ‘new’ TemplateBody @@ -2195,11 +2204,12 @@ object Parsers { * | SimpleExpr `.` MatchClause * | SimpleExpr (TypeArgs | NamedTypeArgs) * | SimpleExpr1 ArgumentExprs - * | SimpleExpr1 `:` nl ArgumentExprs + * | SimpleExpr1 :<<< BlockExpr >>> -- under language.experimental.fewerBraces + * | SimpleExpr1 FunParams (‘=>’ | ‘?=>’) indent Block outdent -- under language.experimental.fewerBraces * Quoted ::= ‘'’ ‘{’ Block ‘}’ * | ‘'’ ‘[’ Type ‘]’ */ - def simpleExpr(): Tree = { + def simpleExpr(location: Location): Tree = { var canApply = true val t = in.token match { case XMLSTART => @@ -2235,7 +2245,7 @@ object Parsers { newExpr() case MACRO => val start = in.skipToken() - MacroTree(simpleExpr()) + MacroTree(simpleExpr(Location.ElseWhere)) case _ => if (isLiteral) literal() else { @@ -2243,29 +2253,57 @@ object Parsers { errorTermTree } } - simpleExprRest(t, canApply) + simpleExprRest(t, location, canApply) } - def simpleExprRest(t: Tree, canApply: Boolean = true): Tree = { + def simpleExprRest(t: Tree, location: Location, canApply: Boolean = true): Tree = { if (canApply) argumentStart() in.token match { case DOT => in.nextToken() - simpleExprRest(selectorOrMatch(t), canApply = true) + simpleExprRest(selectorOrMatch(t), location, canApply = true) case LBRACKET => val tapp = atSpan(startOffset(t), in.offset) { TypeApply(t, typeArgs(namedOK = true, wildOK = false)) } - simpleExprRest(tapp, canApply = true) + simpleExprRest(tapp, location, canApply = true) case LPAREN | LBRACE | INDENT if canApply => - val app = atSpan(startOffset(t), in.offset) { mkApply(t, argumentExprs()) } - simpleExprRest(app, canApply = true) + val inParents = in.token == LPAREN + val app = atSpan(startOffset(t), in.offset) { + val argExprs @ (args, isUsing) = argumentExprs() + if inParents && !isUsing && in.isArrow && location != Location.InGuard 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 USCORE => - if in.lookahead.isArrow then ??? - else atSpan(startOffset(t), in.skipToken()) { PostfixOp(t, Ident(nme.WILDCARD)) } + if in.lookahead.isArrow && location != Location.InGuard 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 => + val app = applyToClosure(t, in.offset, convertToParams(termIdent())) + simpleExprRest(app, location, canApply = true) 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") + val body = inDefScopeBraces(block(simplify = true)) + Function(params, body) + } + Apply(t, arg) + } + /** SimpleExpr ::= ‘new’ ConstrApp {`with` ConstrApp} [TemplateBody] * | ‘new’ TemplateBody */ @@ -2382,7 +2420,7 @@ object Parsers { /** Guard ::= if PostfixExpr */ def guard(): Tree = - if (in.token == IF) { in.nextToken(); postfixExpr() } + if (in.token == IF) { in.nextToken(); postfixExpr(Location.InGuard) } else EmptyTree /** Enumerators ::= Generator {semi Enumerator | Guard} @@ -2607,7 +2645,7 @@ object Parsers { /** InfixPattern ::= SimplePattern {id [nl] SimplePattern} */ def infixPattern(): Tree = - infixOps(simplePattern(), in.canStartExprTokens, simplePattern, + infixOps(simplePattern(), in.canStartExprTokens, simplePatternFn, Location.InPattern, isType = false, isOperator = in.name != nme.raw.BAR && !followingIsVararg()) @@ -2622,7 +2660,7 @@ object Parsers { * PatVar ::= id * | `_' */ - val simplePattern: () => Tree = () => in.token match { + def simplePattern(): Tree = in.token match { case IDENTIFIER | BACKQUOTED_IDENT | THIS | SUPER => simpleRef() match case id @ Ident(nme.raw.MINUS) if isNumericLit => literal(startOffset(id)) @@ -2632,7 +2670,7 @@ object Parsers { case LPAREN => atSpan(in.offset) { makeTupleOrParens(inParens(patternsOpt())) } case QUOTE => - simpleExpr() + simpleExpr(Location.ElseWhere) case XMLSTART => xmlLiteralPattern() case GIVEN => @@ -2649,6 +2687,8 @@ object Parsers { } } + val simplePatternFn: Location => Tree = _ => simplePattern() + def simplePatternRest(t: Tree): Tree = if in.token == DOT then in.nextToken() diff --git a/docs/docs/internals/syntax.md b/docs/docs/internals/syntax.md index a7d57858e1b8..ad51743cc8b4 100644 --- a/docs/docs/internals/syntax.md +++ b/docs/docs/internals/syntax.md @@ -244,8 +244,10 @@ SimpleExpr ::= SimpleRef | SimpleExpr ‘.’ MatchClause | SimpleExpr TypeArgs TypeApply(expr, args) | SimpleExpr ArgumentExprs Apply(expr, args) + | SimpleExpr1 :<<< BlockExpr >>> -- under language.experimental.fewerBraces + | SimpleExpr1 FunParams (‘=>’ | ‘?=>’) indent Block outdent -- under language.experimental.fewerBraces | SimpleExpr ‘_’ PostfixOp(expr, _) (to be dropped) - | XmlExpr (to be dropped) + | XmlExpr -- to be dropped Quoted ::= ‘'’ ‘{’ Block ‘}’ | ‘'’ ‘[’ Type ‘]’ ExprsInParens ::= ExprInParens {‘,’ ExprInParens} diff --git a/tests/neg/closure-args.scala b/tests/neg/closure-args.scala new file mode 100644 index 000000000000..46106f9eca92 --- /dev/null +++ b/tests/neg/closure-args.scala @@ -0,0 +1,7 @@ +val x = List().map (x: => Int) => // error + ??? +val y = List() map x => // error + x + 1 // error +val z = List() map + => // error + ??? + diff --git a/tests/neg/i1779.scala b/tests/neg/i1779.scala index 6435d2bc3491..16b8e75b0057 100644 --- a/tests/neg/i1779.scala +++ b/tests/neg/i1779.scala @@ -8,6 +8,6 @@ object Test { def f = { val _parent = 3 q"val hello = $_parent" - q"class $_" // error // error - } // error + q"class $_" // error // error // error + } } diff --git a/tests/neg/i6059.scala b/tests/neg/i6059.scala index a5bf46e591c9..375c99fb6ba0 100644 --- a/tests/neg/i6059.scala +++ b/tests/neg/i6059.scala @@ -1,3 +1,3 @@ def I0(I1: Int ) = I1 -val I1 = I0(I0 i2 // error -) => true \ No newline at end of file +val I1 = I0(I0 i2) => // error + true diff --git a/tests/neg/i7751.scala b/tests/neg/i7751.scala index c67b3edc63c1..f353d7d8d5ec 100644 --- a/tests/neg/i7751.scala +++ b/tests/neg/i7751.scala @@ -1,2 +1,2 @@ -val a = Some(a=a,)=> // error // error +val a = Some(a=a,)=> // error // error // error val a = Some(x=y,)=> diff --git a/tests/pos/closure-args.scala b/tests/pos/closure-args.scala new file mode 100644 index 000000000000..5b3b267d895a --- /dev/null +++ b/tests/pos/closure-args.scala @@ -0,0 +1,14 @@ +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 => + y > 0 + (0) From a57134e415ea943c60a8d03f4b787383745d94c0 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 18 Mar 2021 22:07:38 +0100 Subject: [PATCH 08/14] Enable closure arguments only under fewerBracesEnabled --- .../src/dotty/tools/dotc/parsing/Parsers.scala | 15 +++++++++------ docs/docs/internals/syntax.md | 2 +- tests/neg/closure-args.scala | 2 ++ tests/neg/i7751.scala | 1 + tests/pos/closure-args.scala | 2 ++ 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 78a8619ab179..a9995cab4ebd 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2204,7 +2204,7 @@ object Parsers { * | SimpleExpr `.` MatchClause * | SimpleExpr (TypeArgs | NamedTypeArgs) * | SimpleExpr1 ArgumentExprs - * | SimpleExpr1 :<<< BlockExpr >>> -- under language.experimental.fewerBraces + * | SimpleExpr1 :<<< (CaseClauses | Block) >>> -- under language.experimental.fewerBraces * | SimpleExpr1 FunParams (‘=>’ | ‘?=>’) indent Block outdent -- under language.experimental.fewerBraces * Quoted ::= ‘'’ ‘{’ Block ‘}’ * | ‘'’ ‘[’ Type ‘]’ @@ -2265,11 +2265,10 @@ object Parsers { case LBRACKET => val tapp = atSpan(startOffset(t), in.offset) { TypeApply(t, typeArgs(namedOK = true, wildOK = false)) } simpleExprRest(tapp, location, canApply = true) - case LPAREN | LBRACE | INDENT if canApply => - val inParents = in.token == LPAREN + case LPAREN if canApply => val app = atSpan(startOffset(t), in.offset) { val argExprs @ (args, isUsing) = argumentExprs() - if inParents && !isUsing && in.isArrow && location != Location.InGuard then + 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) @@ -2279,13 +2278,17 @@ object Parsers { mkApply(t, argExprs) } simpleExprRest(app, location, canApply = true) + case 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 then + 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 => + 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) case _ => diff --git a/docs/docs/internals/syntax.md b/docs/docs/internals/syntax.md index ad51743cc8b4..cd5a3d53aed1 100644 --- a/docs/docs/internals/syntax.md +++ b/docs/docs/internals/syntax.md @@ -244,7 +244,7 @@ SimpleExpr ::= SimpleRef | SimpleExpr ‘.’ MatchClause | SimpleExpr TypeArgs TypeApply(expr, args) | SimpleExpr ArgumentExprs Apply(expr, args) - | SimpleExpr1 :<<< BlockExpr >>> -- under language.experimental.fewerBraces + | SimpleExpr1 :<<< (CaseClauses | Block) >>> -- under language.experimental.fewerBraces | SimpleExpr1 FunParams (‘=>’ | ‘?=>’) indent Block outdent -- under language.experimental.fewerBraces | SimpleExpr ‘_’ PostfixOp(expr, _) (to be dropped) | XmlExpr -- to be dropped diff --git a/tests/neg/closure-args.scala b/tests/neg/closure-args.scala index 46106f9eca92..9b59de205d86 100644 --- a/tests/neg/closure-args.scala +++ b/tests/neg/closure-args.scala @@ -1,3 +1,5 @@ +import language.experimental.fewerBraces + val x = List().map (x: => Int) => // error ??? val y = List() map x => // error diff --git a/tests/neg/i7751.scala b/tests/neg/i7751.scala index f353d7d8d5ec..ed33723a152d 100644 --- a/tests/neg/i7751.scala +++ b/tests/neg/i7751.scala @@ -1,2 +1,3 @@ +import language.experimental.fewerBraces val a = Some(a=a,)=> // error // error // error val a = Some(x=y,)=> diff --git a/tests/pos/closure-args.scala b/tests/pos/closure-args.scala index 5b3b267d895a..dd30baf7d2b0 100644 --- a/tests/pos/closure-args.scala +++ b/tests/pos/closure-args.scala @@ -1,3 +1,5 @@ +import language.experimental.fewerBraces + val xs = List(1, 2, 3) val ys = xs.map x => x + 1 From db19cfd5ff3374fda7cdc1a2fdcec38be65fd36f Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 19 Mar 2021 14:27:57 +0100 Subject: [PATCH 09/14] Allow to retract to new column if next token is `:` This allows a useful pattern for expressing curried function arguments with `:`. E.g. files.get(fileName).fold: "not found" : f => f.readLine() Without the change here, the second `:` would have to be on the same column as `files`. --- compiler/src/dotty/tools/dotc/parsing/Scanners.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 3ed3a8b980a1..d79fe5b35200 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -539,10 +539,10 @@ object Scanners { currentRegion.knownWidth = nextWidth else if (lastWidth != nextWidth) errorButContinue(spaceTabMismatchMsg(lastWidth, nextWidth)) - currentRegion match { + currentRegion match case Indented(curWidth, others, prefix, outer) if curWidth < nextWidth && !others.contains(nextWidth) && nextWidth != lastWidth => - if (token == OUTDENT) + if token == OUTDENT && next.token != COLON then errorButContinue( i"""The start of this line does not match any of the previous indentation widths. |Indentation width of current line : $nextWidth @@ -550,7 +550,6 @@ object Scanners { else currentRegion = Indented(curWidth, others + nextWidth, prefix, outer) case _ => - } end handleNewLine def spaceTabMismatchMsg(lastWidth: IndentWidth, nextWidth: IndentWidth) = From 0bd9739fc1e647766cf30c4b00e699d5571ad72f Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 19 Mar 2021 14:30:11 +0100 Subject: [PATCH 10/14] Disallow `:` after infix operators Two reasons: 1. It would complicate the syntax 2. It looks weird anyway --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index a9995cab4ebd..dd829389e49e 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -934,7 +934,6 @@ object Parsers { val op = if (isType) 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) if (maybePostfix && !canStartOperand(in.token)) { val topInfo = opStack.head From e5fd587b92e254be0a6340122f11db170c397837 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 19 Mar 2021 14:31:43 +0100 Subject: [PATCH 11/14] Improve error recovery after illegal `:` Some users might write things like `then:` by accident. In this case we should report an error, skip the `:`, and continue. No skip ahead to an anchor token is wanted. --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index dd829389e49e..32949477f3e7 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2246,11 +2246,15 @@ object Parsers { val start = in.skipToken() MacroTree(simpleExpr(Location.ElseWhere)) case _ => - if (isLiteral) literal() - else { + if isLiteral then + literal() + else if in.isColon() then + syntaxError(IllegalStartSimpleExpr(tokenString(in.token))) + in.nextToken() + simpleExpr(location) + else syntaxErrorOrIncomplete(IllegalStartSimpleExpr(tokenString(in.token)), expectedOffset) errorTermTree - } } simpleExprRest(t, location, canApply) } From 0c57527b1fbcef11f017b615e7751b8e27485b0e Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 19 Mar 2021 14:31:54 +0100 Subject: [PATCH 12/14] More tests --- .../indentation-experimental.md | 77 +++++++++++++++++++ tests/neg/indent-colons.scala | 7 ++ tests/neg/indent-experimental.scala | 15 ++++ tests/pos/indent-colons.scala | 24 +++++- 4 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 docs/docs/reference/other-new-features/indentation-experimental.md create mode 100644 tests/neg/indent-colons.scala create mode 100644 tests/neg/indent-experimental.scala diff --git a/docs/docs/reference/other-new-features/indentation-experimental.md b/docs/docs/reference/other-new-features/indentation-experimental.md new file mode 100644 index 000000000000..25d017de63d0 --- /dev/null +++ b/docs/docs/reference/other-new-features/indentation-experimental.md @@ -0,0 +1,77 @@ +--- +layout: doc-page +title: "Fewer Braces" +--- + +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. It also allows to leave out braces around arguments that are multi-line function values. + +## 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") +``` + +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 + +Braces can also be omitted around multiple line function value arguments. Examples +```scala +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. + +## Syntax Changes + +``` +SimpleExpr ::= ... + | SimpleExpr : indent (CaseClauses | Block) outdent + | SimpleExpr FunParams (‘=>’ | ‘?=>’) indent Block outdent +``` + +Note that indented blocks after `:` or `=>` only work when following a simple expression, they are not allowed after an infix operator. So the following examples +would be incorrect: + +```scala + x + : // error + y + + f `andThen` y => // error + y + 1 +``` + +Note also 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: + +```scala + xs.map x => x + 1 // error: braces or parentheses are required + xs.map(x => x + 1) // ok +``` diff --git a/tests/neg/indent-colons.scala b/tests/neg/indent-colons.scala new file mode 100644 index 000000000000..9142ddce2d7c --- /dev/null +++ b/tests/neg/indent-colons.scala @@ -0,0 +1,7 @@ +import language.experimental.fewerBraces + +val x = 1.+ : // ok + 2 + +val y = 1 + : // error // error + 2 // error diff --git a/tests/neg/indent-experimental.scala b/tests/neg/indent-experimental.scala new file mode 100644 index 000000000000..c30dd9751833 --- /dev/null +++ b/tests/neg/indent-experimental.scala @@ -0,0 +1,15 @@ +import language.experimental.fewerBraces + +val x = + if true then: // error + 1 + else: // error + 2 + + +val credentials = List("OK") +val all = credentials ++ : // error + val file = "file" // error + if file.isEmpty // error + then Seq("none") + else Seq(file) diff --git a/tests/pos/indent-colons.scala b/tests/pos/indent-colons.scala index aa611e796fba..2506dd1bf415 100644 --- a/tests/pos/indent-colons.scala +++ b/tests/pos/indent-colons.scala @@ -116,4 +116,26 @@ class Coder(words: List[String]): end Coder object Test22: - def foo: Int = 22 \ No newline at end of file + def foo: Int = 22 + +def tryEither[T](x: T)(y: Int => T): T = ??? + +def test1 = + tryEither: + "hello" + : + y => y.toString + +def test2 = + tryEither: + "hello" + : + _.toString + + +val o = + Some(3).fold: + "nothing" + : + x => x.toString + From 8cd633758e422e70fccad7da7cb84d7343b7bcae Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 19 Mar 2021 16:26:01 +0100 Subject: [PATCH 13/14] Augment returns test --- tests/run/returns.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/run/returns.scala b/tests/run/returns.scala index e329035f5a79..9b04d25bb26c 100644 --- a/tests/run/returns.scala +++ b/tests/run/returns.scala @@ -1,7 +1,8 @@ def foo(x: Int): Int = if x == 1 then return - x + val y = 2 + x + y else 2 @@ -10,7 +11,7 @@ def bar(): Unit = assert(false) @main def Test = - assert(foo(1) == 1) + assert(foo(1) == 3) bar() From 9fd873ceca9691a469918af5c08d8dfd5b547f16 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 19 Mar 2021 17:43:57 +0100 Subject: [PATCH 14/14] Revert "Disallow `:` after infix operators" This reverts commit 0bd9739fc1e647766cf30c4b00e699d5571ad72f. Change syntax and tests to allow `:` after operator. It turns out that the intent project of the CB already uses them in a way that looks quite reasonable. --- .../dotty/tools/dotc/parsing/Parsers.scala | 13 ++++--- docs/docs/internals/syntax.md | 8 ++-- .../indentation-experimental.md | 37 +++++++++++-------- tests/neg/indent-colons.scala | 7 ---- tests/neg/indent-experimental.scala | 7 ---- tests/pos/indent-colons.scala | 27 ++++++++++++++ 6 files changed, 60 insertions(+), 39 deletions(-) delete mode 100644 tests/neg/indent-colons.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 32949477f3e7..670cd377f16c 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -934,6 +934,7 @@ object Parsers { val op = if (isType) 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) if (maybePostfix && !canStartOperand(in.token)) { val topInfo = opStack.head @@ -2160,6 +2161,7 @@ object Parsers { /** PostfixExpr ::= InfixExpr [id [nl]] * InfixExpr ::= PrefixExpr * | InfixExpr id [nl] InfixExpr + * | InfixExpr id `:` IndentedExpr * | InfixExpr MatchClause */ def postfixExpr(location: Location = Location.ElseWhere): Tree = @@ -2203,8 +2205,9 @@ object Parsers { * | SimpleExpr `.` MatchClause * | SimpleExpr (TypeArgs | NamedTypeArgs) * | SimpleExpr1 ArgumentExprs - * | SimpleExpr1 :<<< (CaseClauses | Block) >>> -- under language.experimental.fewerBraces - * | SimpleExpr1 FunParams (‘=>’ | ‘?=>’) indent Block outdent -- under language.experimental.fewerBraces + * | SimpleExpr1 `:` IndentedExpr -- under language.experimental.fewerBraces + * | SimpleExpr1 FunParams (‘=>’ | ‘?=>’) IndentedExpr -- under language.experimental.fewerBraces + * IndentedExpr ::= indent (CaseClauses | Block) outdent * Quoted ::= ‘'’ ‘{’ Block ‘}’ * | ‘'’ ‘[’ Type ‘]’ */ @@ -2304,8 +2307,7 @@ object Parsers { val arg = atSpan(start, in.skipToken()) { if in.token != INDENT then syntaxErrorOrIncomplete(i"indented expression expected, ${in} found") - val body = inDefScopeBraces(block(simplify = true)) - Function(params, body) + Function(params, blockExpr()) } Apply(t, arg) } @@ -2397,8 +2399,7 @@ object Parsers { else fn } - /** BlockExpr ::= `{' BlockExprContents `}' - * BlockExprContents ::= CaseClauses | Block + /** BlockExpr ::= <<< (CaseClauses | Block) >>> */ def blockExpr(): Tree = atSpan(in.offset) { val simplify = in.token == INDENT diff --git a/docs/docs/internals/syntax.md b/docs/docs/internals/syntax.md index cd5a3d53aed1..c6419090336f 100644 --- a/docs/docs/internals/syntax.md +++ b/docs/docs/internals/syntax.md @@ -227,6 +227,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 MatchClause MatchClause ::= ‘match’ <<< CaseClauses >>> Match(expr, cases) PrefixExpr ::= [‘-’ | ‘+’ | ‘~’ | ‘!’] SimpleExpr PrefixOp(expr, op) @@ -244,10 +245,11 @@ SimpleExpr ::= SimpleRef | SimpleExpr ‘.’ MatchClause | SimpleExpr TypeArgs TypeApply(expr, args) | SimpleExpr ArgumentExprs Apply(expr, args) - | SimpleExpr1 :<<< (CaseClauses | Block) >>> -- under language.experimental.fewerBraces - | SimpleExpr1 FunParams (‘=>’ | ‘?=>’) indent Block outdent -- under language.experimental.fewerBraces + | SimpleExpr1 ‘:’ IndentedExpr -- under language.experimental.fewerBraces + | SimpleExpr1 FunParams (‘=>’ | ‘?=>’) IndentedExpr -- under language.experimental.fewerBraces | SimpleExpr ‘_’ PostfixOp(expr, _) (to be dropped) | XmlExpr -- to be dropped +IndentedExpr ::= indent CaseClauses | Block outdent Quoted ::= ‘'’ ‘{’ Block ‘}’ | ‘'’ ‘[’ Type ‘]’ ExprsInParens ::= ExprInParens {‘,’ ExprInParens} @@ -257,7 +259,7 @@ ParArgumentExprs ::= ‘(’ [‘using’] ExprsInParens ‘)’ | ‘(’ [ExprsInParens ‘,’] PostfixExpr ‘*’ ‘)’ exprs :+ Typed(expr, Ident(wildcardStar)) ArgumentExprs ::= ParArgumentExprs | BlockExpr -BlockExpr ::= <<< (CaseClauses | Block) >>> +BlockExpr ::= <<< CaseClauses | Block >>> Block ::= {BlockStat semi} [BlockResult] Block(stats, expr?) BlockStat ::= Import | {Annotation {nl}} {LocalModifier} Def diff --git a/docs/docs/reference/other-new-features/indentation-experimental.md b/docs/docs/reference/other-new-features/indentation-experimental.md index 25d017de63d0..48bc271df67f 100644 --- a/docs/docs/reference/other-new-features/indentation-experimental.md +++ b/docs/docs/reference/other-new-features/indentation-experimental.md @@ -23,6 +23,16 @@ times(10): 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 @@ -39,7 +49,7 @@ val firstLine = files.get(fileName).fold: ## Lambda Arguments Without Braces -Braces can also be omitted around multiple line function value arguments. Examples +Braces can also be omitted around multiple line function value arguments: ```scala val xs = elems.map x => val y = x - 1 @@ -52,26 +62,21 @@ Braces can be omitted if the lambda starts with a parameter list and `=>` or `=> ## Syntax Changes ``` -SimpleExpr ::= ... - | SimpleExpr : indent (CaseClauses | Block) outdent - | SimpleExpr FunParams (‘=>’ | ‘?=>’) indent Block outdent +SimpleExpr ::= ... + | SimpleExpr `:` IndentedArgument + | SimpleExpr FunParams (‘=>’ | ‘?=>’) IndentedArgument +InfixExpr ::= ... + | InfixExpr id `:` IndentedArgument +IndentedArgument ::= indent (CaseClauses | Block) outdent ``` -Note that indented blocks after `:` or `=>` only work when following a simple expression, they are not allowed after an infix operator. So the following examples -would be incorrect: - -```scala - x + : // error - y - - f `andThen` y => // error - y + 1 -``` - -Note also that a lambda argument must have the `=>` at the end of a line for braces +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: ```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 ``` diff --git a/tests/neg/indent-colons.scala b/tests/neg/indent-colons.scala deleted file mode 100644 index 9142ddce2d7c..000000000000 --- a/tests/neg/indent-colons.scala +++ /dev/null @@ -1,7 +0,0 @@ -import language.experimental.fewerBraces - -val x = 1.+ : // ok - 2 - -val y = 1 + : // error // error - 2 // error diff --git a/tests/neg/indent-experimental.scala b/tests/neg/indent-experimental.scala index c30dd9751833..e945e172d1de 100644 --- a/tests/neg/indent-experimental.scala +++ b/tests/neg/indent-experimental.scala @@ -6,10 +6,3 @@ val x = else: // error 2 - -val credentials = List("OK") -val all = credentials ++ : // error - val file = "file" // error - if file.isEmpty // error - then Seq("none") - else Seq(file) diff --git a/tests/pos/indent-colons.scala b/tests/pos/indent-colons.scala index 2506dd1bf415..09abc69a483a 100644 --- a/tests/pos/indent-colons.scala +++ b/tests/pos/indent-colons.scala @@ -139,3 +139,30 @@ val o = : x => x.toString +object Test23: + val x = 1.+ : // ok + 2 + + val y = 1 + : // ok + 2 + + val r = 1 to: + 100 + + val credentials = List("OK") + val all = credentials ++ : + val file = "file" + if file.isEmpty + then Seq("none") + else Seq(file) + +extension (x: Boolean) + infix def or (y: => Boolean) = x || y + +def test24(x: Int, y: Int) = + x < y or: + x > y + or: + x == y + +