From e108486c1c5ad4d67516f36ebb2ea1345719c8fd Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 24 Dec 2020 10:00:25 +0100 Subject: [PATCH 1/4] Eliminate WITHEOL token Change parsing algorithm so that we do not need a separate WITHEOL token. The grammar remains unaffected. --- .../dotty/tools/dotc/parsing/Parsers.scala | 20 +++++++++---------- .../dotty/tools/dotc/parsing/Scanners.scala | 15 ++++++-------- .../src/dotty/tools/dotc/parsing/Tokens.scala | 7 +++---- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index a07230eaa675..9849400da403 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1283,7 +1283,7 @@ object Parsers { def possibleTemplateStart(isNew: Boolean = false): Unit = in.observeColonEOL() - if in.token == COLONEOL || in.token == WITHEOL then + if in.token == COLONEOL || in.token == WITH then if in.lookahead.isIdent(nme.end) then in.token = NEWLINE else in.nextToken() @@ -3545,7 +3545,7 @@ object Parsers { ValDef(name, parents.head, subExpr()) else DefDef(name, tparams, vparamss, parents.head, subExpr()) - else if in.token != WITH && in.token != WITHEOL && parentsIsType then + else if in.token != WITH && parentsIsType then if name.isEmpty then syntaxError(em"anonymous given cannot be abstract") DefDef(name, tparams, vparamss, parents.head, EmptyTree) @@ -3633,12 +3633,12 @@ object Parsers { /** `{`with` ConstrApp} but no EOL allowed after `with`. */ def withConstrApps(): List[Tree] = - if in.token == WITH then - in.observeWithEOL() // converts token to WITHEOL if at end of line - if in.token == WITH && in.lookahead.token != LBRACE then - in.nextToken() - constrApp() :: withConstrApps() - else Nil + def isTemplateStart = + val la = in.lookahead + la.isAfterLineEnd || la.token == LBRACE + if in.token == WITH && !isTemplateStart then + in.nextToken() + constrApp() :: withConstrApps() else Nil /** Template ::= InheritClauses [TemplateBody] @@ -3708,8 +3708,8 @@ object Parsers { /** with Template, with EOL interpreted */ def withTemplate(constr: DefDef, parents: List[Tree]): Template = - if in.token != WITHEOL then accept(WITH) - possibleTemplateStart() // consumes a WITHEOL token + if in.token != WITH then syntaxError(em"`with` expected") + possibleTemplateStart() // consumes a WITH token val (self, stats) = templateBody() Template(constr, parents, Nil, self, stats) .withSpan(Span(constr.span.orElse(parents.head.span).start, in.lastOffset)) diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 164ca41d7cd1..ee75a43db291 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -74,6 +74,9 @@ object Scanners { def isNestedStart = token == LBRACE || token == INDENT def isNestedEnd = token == RBRACE || token == OUTDENT + + /** Is current token first one after a newline? */ + def isAfterLineEnd: Boolean = lineOffset >= 0 } abstract class ScannerCommon(source: SourceFile)(using Context) extends CharArrayReader with TokenData { @@ -512,15 +515,12 @@ object Scanners { |Previous indent : $lastWidth |Latest indent : $nextWidth""" - private def switchAtEOL(testToken: Token, eolToken: Token): Unit = - if token == testToken then + def observeColonEOL(): Unit = + if token == COLON then lookAhead() val atEOL = isAfterLineEnd || token == EOF reset() - if atEOL then token = eolToken - - def observeColonEOL(): Unit = switchAtEOL(COLON, COLONEOL) - def observeWithEOL(): Unit = switchAtEOL(WITH, WITHEOL) + if atEOL then token = COLONEOL def observeIndented(): Unit = if indentSyntax && isNewLine then @@ -610,9 +610,6 @@ object Scanners { } } - /** Is current token first one after a newline? */ - def isAfterLineEnd: Boolean = lineOffset >= 0 - /** Is there a blank line between the current token and the last one? * A blank line consists only of characters <= ' '. * @pre afterLineEnd(). diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index 897fa0bf2957..123d5375912d 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -204,8 +204,7 @@ object Tokens extends TokensCommon { final val QUOTE = 87; enter(QUOTE, "'") final val COLONEOL = 88; enter(COLONEOL, ":", ": at eol") - final val WITHEOL = 89; enter(WITHEOL, "with", "with at eol") - final val SELFARROW = 90; enter(SELFARROW, "=>") // reclassified ARROW following self-type + final val SELFARROW = 89; enter(SELFARROW, "=>") // reclassified ARROW following self-type /** XML mode */ final val XMLSTART = 99; enter(XMLSTART, "$XMLSTART$<") // TODO: deprecate @@ -277,8 +276,8 @@ object Tokens extends TokensCommon { final val closingRegionTokens = BitSet(RBRACE, RPAREN, RBRACKET, CASE) | statCtdTokens final val canStartIndentTokens: BitSet = - statCtdTokens | BitSet(COLONEOL, WITHEOL, EQUALS, ARROW, LARROW, WHILE, TRY, FOR, IF) - // `if` is excluded because it often comes after `else` which makes for awkward indentation rules TODO: try to do without the exception + statCtdTokens | BitSet(COLONEOL, WITH, EQUALS, ARROW, LARROW, WHILE, TRY, FOR, IF) + // TODO: add THROW, CTXARROW /** Faced with the choice between a type and a formal parameter, the following * tokens determine it's a formal parameter. From 0bc44b0b4072a3e8439047b82f053e650be393f6 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 24 Dec 2020 10:12:44 +0100 Subject: [PATCH 2/4] Make indentation significant after `?=>` and `throw` --- compiler/src/dotty/tools/dotc/parsing/Tokens.scala | 5 ++--- .../docs/reference/other-new-features/indentation.md | 4 ++-- tests/pos/indent.scala | 12 ++++++++++-- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index 123d5375912d..c5f77533094d 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -276,8 +276,7 @@ object Tokens extends TokensCommon { final val closingRegionTokens = BitSet(RBRACE, RPAREN, RBRACKET, CASE) | statCtdTokens final val canStartIndentTokens: BitSet = - statCtdTokens | BitSet(COLONEOL, WITH, EQUALS, ARROW, LARROW, WHILE, TRY, FOR, IF) - // TODO: add THROW, CTXARROW + statCtdTokens | BitSet(COLONEOL, WITH, EQUALS, ARROW, CTXARROW, LARROW, WHILE, TRY, FOR, IF, THROW) /** Faced with the choice between a type and a formal parameter, the following * tokens determine it's a formal parameter. @@ -286,7 +285,7 @@ object Tokens extends TokensCommon { final val scala3keywords = BitSet(ENUM, ERASED, GIVEN) - final val endMarkerTokens = identifierTokens | BitSet(IF, WHILE, FOR, MATCH, TRY, NEW, GIVEN, VAL, THIS) + final val endMarkerTokens = identifierTokens | BitSet(IF, WHILE, FOR, MATCH, TRY, NEW, THROW, GIVEN, VAL, THIS) final val softModifierNames = Set(nme.inline, nme.opaque, nme.open, nme.transparent, nme.infix) } diff --git a/docs/docs/reference/other-new-features/indentation.md b/docs/docs/reference/other-new-features/indentation.md index 6c5d6a3617f9..6949f3d75c07 100644 --- a/docs/docs/reference/other-new-features/indentation.md +++ b/docs/docs/reference/other-new-features/indentation.md @@ -64,8 +64,8 @@ There are two rules: - after one of the following tokens: ``` - = => <- catch do else finally for - if match return then try while yield + = => ?=> <- catch do else finally for + if match return then throw try while yield ``` If an `` is inserted, the indentation width of the token on the next line diff --git a/tests/pos/indent.scala b/tests/pos/indent.scala index 3428011ea4fe..03502a26fe79 100644 --- a/tests/pos/indent.scala +++ b/tests/pos/indent.scala @@ -51,6 +51,10 @@ object Test: y * y } + val gg = (x: Int) ?=> + val y = x + 1 + y + xs.map { x => val y = x * x @@ -80,11 +84,15 @@ object Test: class Test2: self => - def foo = 1 + def foo(x: Int) = + if x < 0 then throw + val ex = new AssertionError() + ex + x val x = new Test2 { - override def foo = 2 + override def foo(x: Int) = 2 } end x end Test2 From 0c772e2fdafa2508ec7d2c135f6bd475e7ac328c Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 24 Dec 2020 10:13:52 +0100 Subject: [PATCH 3/4] Fix doc page layout --- .../changed-features/match-syntax.md | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/docs/docs/reference/changed-features/match-syntax.md b/docs/docs/reference/changed-features/match-syntax.md index ed4ec87e2380..149788ff75e5 100644 --- a/docs/docs/reference/changed-features/match-syntax.md +++ b/docs/docs/reference/changed-features/match-syntax.md @@ -6,42 +6,41 @@ title: Match Expressions The syntactical precedence of match expressions has been changed. `match` is still a keyword, but it is used like an alphabetical operator. This has several consequences: -1. `match` expressions can be chained: - - ```scala - xs match { - case Nil => "empty" - case x :: xs1 => "nonempty" - } match { - case "empty" => 0 - case "nonempty" => 1 - } - ``` - - (or, dropping the optional braces) - - ```scala - xs match - case Nil => "empty" - case x :: xs1 => "nonempty" - match - case "empty" => 0 - case "nonempty" => 1 - ``` - -2. `match` may follow a period: + 1. `match` expressions can be chained: ```scala - if xs.match - case Nil => false - case _ => true - then "nonempty" - else "empty" + xs match { + case Nil => "empty" + case x :: xs1 => "nonempty" + } match { + case "empty" => 0 + case "nonempty" => 1 + } ``` -3. The scrutinee of a match expression must be an `InfixExpr`. Previously the scrutinee could be - followed by a type ascription `: T`, but this is no longer supported. So `x : T match { ... }` - now has to be written `(x: T) match { ... }`. + (or, dropping the optional braces) + + ```scala + xs match + case Nil => "empty" + case x :: xs1 => "nonempty" + match + case "empty" => 0 + case "nonempty" => 1 + ``` + + 2. `match` may follow a period: + + ```scala + if xs.match + case Nil => false + case _ => true + then "nonempty" + else "empty" + ``` + + 3. The scrutinee of a match expression must be an `InfixExpr`. Previously the scrutinee could be followed by a type ascription `: T`, but this is no longer supported. So `x : T match { ... }` now has to be + written `(x: T) match { ... }`. ## Syntax From 625f7540a325e4d13781fdd2785d3308247e07b2 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 24 Dec 2020 12:02:12 +0100 Subject: [PATCH 4/4] Add section on Matchable and Universal Equality --- .../reference/other-new-features/matchable.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/docs/reference/other-new-features/matchable.md b/docs/docs/reference/other-new-features/matchable.md index ea2c3cd70587..cdf31e3793c7 100644 --- a/docs/docs/reference/other-new-features/matchable.md +++ b/docs/docs/reference/other-new-features/matchable.md @@ -94,3 +94,45 @@ class Object extends Any, Matchable `Matchable` is currently a marker trait without any methods. Over time we might migrate methods `getClass` and `isInstanceOf` to it, since these are closely related to pattern-matching. + +### `Matchable` and Universal Equality + +Methods that pattern-match on selectors of type `Any` will need a cast once the +Matchable warning is turned on. The most common such method is the universal +`equals` method. It will have to be written as in the following example: + +```scala +class C(val x: String): + + override def equals(that: Any): Boolean = + that.asInstanceOf[Matchable] match + case that: C => this.x == that.x + case _ => false +``` +The cast of `that` to `Matchable` serves as an indication that universal equality +is unsafe in the presence of abstract types and opaque types since it cannot properly distinguish the meaning of a type from its representation. The cast +is guaranteed to succeed at run-time since `Any` and `Matchable` both erase to +`Object`. + +For instance, consider the definitions +```scala +opaque type Meter = Double +def Meter(x: Double) = x + +opaque type Second = Double +def Second(x: Double) = x +``` +Here, universal `equals` will return true for +```scala + Meter(10).equals(Second(10)) +``` +even though this is clearly false mathematically. With [multiversal equality](../contextual/multiversal-equality.html) one can mitigate that problem somewhat by turning +```scala + Meter(10) == Second(10) +``` +into a type error. + + + + +