From 869c8ee9b247aae7d840aa131e206a0923ac8e8a Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 1 Jan 2021 17:42:11 +0100 Subject: [PATCH 1/3] Allow indentation to work inside parens Actually indentation was always enabled inside parens, but the rule which indentation width to use was too simplistic and often required an extra line. This is now fixed, so both of these work: ```scala def test1 = g(1, x => val y = x * x y * y ) def test2 = g(1, x => val y = x * x y * y ) ``` --- .../dotty/tools/dotc/parsing/Scanners.scala | 14 +++++++- .../other-new-features/indentation.md | 32 +++++++++++++++++-- tests/pos/indent-in-parens.scala | 13 ++++++++ tests/run/Course-2002-13.scala | 2 +- 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index ee75a43db291..3783e28e6706 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -462,7 +462,7 @@ object Scanners { indentPrefix = r.prefix case r => indentIsSignificant = indentSyntax - if (r.knownWidth == null) r.knownWidth = nextWidth + r.proposeKnownWidth(nextWidth, lastToken) lastWidth = r.knownWidth newlineIsSeparating = r.isInstanceOf[InBraces] @@ -1349,6 +1349,18 @@ object Scanners { /** The indentation width, Zero if not known */ final def indentWidth: IndentWidth = if knownWidth == null then IndentWidth.Zero else knownWidth + + def proposeKnownWidth(width: IndentWidth, lastToken: Token) = + if knownWidth == null then + this match + case InParens(_, _) if lastToken != LPAREN => + useOuterWidth() + case _ => + knownWidth = width + + private def useOuterWidth(): Unit = + if enclosing.knownWidth == null then enclosing.useOuterWidth() + knownWidth = enclosing.knownWidth end Region case class InString(multiLine: Boolean, outer: Region) extends Region diff --git a/docs/docs/reference/other-new-features/indentation.md b/docs/docs/reference/other-new-features/indentation.md index 6949f3d75c07..4c0778ed229e 100644 --- a/docs/docs/reference/other-new-features/indentation.md +++ b/docs/docs/reference/other-new-features/indentation.md @@ -159,13 +159,39 @@ Indentation prefixes can consist of spaces and/or tabs. Indentation widths are t ### Indentation and Braces -Indentation can be mixed freely with braces. For interpreting indentation inside braces, the following rules apply. +Indentation can be mixed freely with braces and parentheses. For interpreting indentation inside braces and parentheses, the following rules apply. 1. The assumed indentation width of a multiline region enclosed in braces is the indentation width of the first token that starts a new line after the opening brace. - 2. On encountering a closing brace `}`, as many `` tokens as necessary are - inserted to close all open indentation regions inside the pair of braces. + 2. The assumed indentation width of a multiline region inside parentheses is: + + - if the opening parenthesis is at the end of a line, the indentation width of token following it, + - otherwise, the indentation width of the enclosing region. + + 3. On encountering a closing brace `}` or parenthesis `)`, as many `` tokens as necessary are + inserted to close all open indentation regions inside the pair of braces or parentheses. + +For instance, consider: +```scala +{ + val x = f(x: Int, y => + x * ( + y + 1 + ) + + (x + + x) + ) +} +``` + - Here, the indentation width of the region enclosed by the braces is 3 (i.e. the indentation width of the +statement starting with `val`). + - The indentation width of the region in parentheses that follows `f` is also 3, since the opening + parenthesis is not at the end of a line. + - The indentation width of the region in parentheses around `y + 1` is 9 + (i.e. the indentation width of `y + 1`). + - Finally, the indentation width of the last region in parentheses starting with `(x` is 6 (i.e. the indentation +width of the indented region following the `=>`. ### Special Treatment of Case Clauses diff --git a/tests/pos/indent-in-parens.scala b/tests/pos/indent-in-parens.scala index 946b7b22d832..ae01b631314e 100644 --- a/tests/pos/indent-in-parens.scala +++ b/tests/pos/indent-in-parens.scala @@ -1,3 +1,16 @@ +def g(x: Int, op: Int => Int) = op(x) + +def test1 = g(1, x => + val y = x * x + y * y + ) + +def test2 = g(1, + x => + val y = x * x + y * y + ) + def f(x: Int) = assert( if x > 0 then diff --git a/tests/run/Course-2002-13.scala b/tests/run/Course-2002-13.scala index e908a557a768..8a2dd9575410 100644 --- a/tests/run/Course-2002-13.scala +++ b/tests/run/Course-2002-13.scala @@ -179,7 +179,7 @@ class Parser(s: String) { val a = token; token = it.next; Con(a, - if (token equals "(") { + if (token equals "(") { token = it.next; val ts: List[Term] = if (token equals ")") List() else rep(term); if (token equals ")") token = it.next else syntaxError("`)` expected"); From 68c762a37a9579da7b0ed8fa0be6fabbc1a82274 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 1 Jan 2021 20:22:00 +0100 Subject: [PATCH 2/3] Allow infix operators on their own line --- .../src/dotty/tools/dotc/parsing/Scanners.scala | 3 ++- docs/docs/reference/changed-features/operators.md | 7 ++++--- .../reference/other-new-features/indentation.md | 12 +++++------- tests/pos/leading-infix-op.scala | 13 +++++++++++++ tests/run/i7031.scala | 3 ++- 5 files changed, 26 insertions(+), 12 deletions(-) create mode 100644 tests/pos/leading-infix-op.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 3783e28e6706..2ff2714cf626 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -345,7 +345,7 @@ object Scanners { allowLeadingInfixOperators && ( token == BACKQUOTED_IDENT || token == IDENTIFIER && isOperatorPart(name(name.length - 1))) - && ch == ' ' + && ch <= ' ' && !pastBlankLine && { val lookahead = LookaheadScanner() @@ -353,6 +353,7 @@ object Scanners { // force a NEWLINE a after current token if it is on its own line lookahead.nextToken() canStartExprTokens.contains(lookahead.token) + || lookahead.token == NEWLINE && canStartExprTokens.contains(lookahead.next.token) } && { if migrateTo3 then diff --git a/docs/docs/reference/changed-features/operators.md b/docs/docs/reference/changed-features/operators.md index d98da3bed317..04fe87976e02 100644 --- a/docs/docs/reference/changed-features/operators.md +++ b/docs/docs/reference/changed-features/operators.md @@ -114,7 +114,8 @@ val str = "hello" def condition = x > 0 - || xs.exists(_ > 0) + || + xs.exists(_ > 0) || xs.isEmpty ``` Previously, those expressions would have been rejected, since the compiler's semicolon inference @@ -124,8 +125,8 @@ To make this syntax work, the rules are modified to not infer semicolons in fron A _leading infix operator_ is - a symbolic identifier such as `+`, or `approx_==`, or an identifier in backticks, - that starts a new line, - - that precedes a token on the same line that can start an expression, - - and that is immediately followed by at least one space character `' '`. + - that precedes a token on the same or the next line that can start an expression, + - and that is immediately followed by at least one whitespace character. Example: diff --git a/docs/docs/reference/other-new-features/indentation.md b/docs/docs/reference/other-new-features/indentation.md index 4c0778ed229e..2980d9d1368a 100644 --- a/docs/docs/reference/other-new-features/indentation.md +++ b/docs/docs/reference/other-new-features/indentation.md @@ -159,18 +159,17 @@ Indentation prefixes can consist of spaces and/or tabs. Indentation widths are t ### Indentation and Braces -Indentation can be mixed freely with braces and parentheses. For interpreting indentation inside braces and parentheses, the following rules apply. +Indentation can be mixed freely with braces `{...}`, as well as brackets `[...]` and parentheses `(...)`. For interpreting indentation inside such regions, the following rules apply. 1. The assumed indentation width of a multiline region enclosed in braces is the indentation width of the first token that starts a new line after the opening brace. - 2. The assumed indentation width of a multiline region inside parentheses is: + 2. The assumed indentation width of a multiline region inside brackets or parentheses is: - - if the opening parenthesis is at the end of a line, the indentation width of token following it, + - if the opening bracket or parenthesis is at the end of a line, the indentation width of token following it, - otherwise, the indentation width of the enclosing region. - 3. On encountering a closing brace `}` or parenthesis `)`, as many `` tokens as necessary are - inserted to close all open indentation regions inside the pair of braces or parentheses. + 3. On encountering a closing brace `}`, bracket `]` or parenthesis `)`, as many `` tokens as necessary are inserted to close all open nested indentation regions. For instance, consider: ```scala @@ -190,8 +189,7 @@ statement starting with `val`). parenthesis is not at the end of a line. - The indentation width of the region in parentheses around `y + 1` is 9 (i.e. the indentation width of `y + 1`). - - Finally, the indentation width of the last region in parentheses starting with `(x` is 6 (i.e. the indentation -width of the indented region following the `=>`. + - Finally, the indentation width of the last region in parentheses starting with `(x` is 6 (i.e. the indentation width of the indented region following the `=>`. ### Special Treatment of Case Clauses diff --git a/tests/pos/leading-infix-op.scala b/tests/pos/leading-infix-op.scala new file mode 100644 index 000000000000..30e18b800fa0 --- /dev/null +++ b/tests/pos/leading-infix-op.scala @@ -0,0 +1,13 @@ +def f(x: Int): Boolean = + x < 0 + || + x > 0 + && + x != 3 + +def g(x: Option[Int]) = x match + case Some(err) => + println("hi") + ??? + case None => + ??? diff --git a/tests/run/i7031.scala b/tests/run/i7031.scala index 0756f885d966..0bc3fe81f9e6 100644 --- a/tests/run/i7031.scala +++ b/tests/run/i7031.scala @@ -1,9 +1,10 @@ @main def Test = { val a = 5 val x = 1 + + // `a` * 6 - assert(x == 1) + assert(x == 1, x) } From f767c00e37d2126a1e30af0a8ae13c3ee3042fcf Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Wed, 6 Jan 2021 13:40:33 +0100 Subject: [PATCH 3/3] Address review comments --- compiler/src/dotty/tools/dotc/parsing/Scanners.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 2ff2714cf626..aae4aab4e588 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -337,15 +337,15 @@ object Scanners { /** A leading symbolic or backquoted identifier is treated as an infix operator if * - it does not follow a blank line, and - * - it is followed on the same line by at least one ' ' - * and a token that can start an expression. + * - it is followed by at least one whitespace character and a + * token that can start an expression. * If a leading infix operator is found and the source version is `3.0-migration`, emit a change warning. */ def isLeadingInfixOperator(inConditional: Boolean = true) = allowLeadingInfixOperators && ( token == BACKQUOTED_IDENT || token == IDENTIFIER && isOperatorPart(name(name.length - 1))) - && ch <= ' ' + && (isWhitespace(ch) || ch == LF) && !pastBlankLine && { val lookahead = LookaheadScanner()