diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 9d47db6171c1..62d9a086eb63 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -951,7 +951,7 @@ object Parsers { recur(top) } - /** operand { infixop operand | ‘given’ (operand | ParArgumentExprs) } [postfixop], + /** operand { infixop operand | MatchClause } [postfixop], * * respecting rules of associativity and precedence. * @param isOperator the current token counts as an operator. @@ -981,18 +981,14 @@ object Parsers { } else recur(operand()) } - else reduceStack(base, top, minPrec, leftAssoc = true, in.name, isType) + else + val t = reduceStack(base, top, minPrec, leftAssoc = true, in.name, isType) + if !isType && in.token == MATCH then recur(matchClause(t)) + else t recur(first) } - def applyGiven(t: Tree, operand: () => Tree): Tree = - atSpan(startOffset(t), in.offset) { - in.nextToken() - val args = if (in.token == LPAREN) parArgumentExprs()._1 else operand() :: Nil - Apply(t, args) - }.setGivenApply() - /* -------- IDENTIFIERS AND LITERALS ------------------------------------------- */ /** Accept identifier and return its name as a term name. */ @@ -1028,9 +1024,11 @@ object Parsers { def wildcardIdent(): Ident = atSpan(accept(USCORE)) { Ident(nme.WILDCARD) } - /** Accept identifier acting as a selector on given tree `t`. */ + /** Accept identifier or match clause acting as a selector on given tree `t` */ def selector(t: Tree): Tree = - atSpan(startOffset(t), in.offset) { Select(t, ident()) } + atSpan(startOffset(t), in.offset) { + if in.token == MATCH then matchClause(t) else Select(t, ident()) + } /** Selectors ::= id { `.' id } * @@ -1737,11 +1735,10 @@ object Parsers { * | HkTypeParamClause ‘=>’ Expr * | [SimpleExpr `.'] id `=' Expr * | SimpleExpr1 ArgumentExprs `=' Expr - * | Expr2 - * | [‘inline’] Expr2 `match' (`{' CaseClauses `}' | CaseClause) + * | PostfixExpr [Ascription] + * | ‘inline’ InfixExpr MatchClause * Bindings ::= `(' [Binding {`,' Binding}] `)' * Binding ::= (id | `_') [`:' Type] - * Expr2 ::= PostfixExpr [Ascription] * Ascription ::= `:' InfixType * | `:' Annotation {Annotation} * | `:' `_' `*' @@ -1780,7 +1777,7 @@ object Parsers { } } - def expr1(location: Location.Value = Location.ElseWhere): Tree = in.token match { + def expr1(location: Location.Value = Location.ElseWhere): Tree = in.token match case IF => in.endMarkerScope(IF) { ifExpr(in.offset, If) } case WHILE => @@ -1886,31 +1883,28 @@ object Parsers { case IF => ifExpr(start, InlineIf) case _ => - val t = postfixExpr() - if (in.token == MATCH) matchExpr(t, start, InlineMatch) - else - syntaxErrorOrIncomplete(i"`match` or `if` expected but ${in.token} found") - t + postfixExpr() match + case t @ Match(scrut, cases) => + InlineMatch(scrut, cases).withSpan(t.span) + case t => + syntaxError(em"`inline` must be followed by an `if` or a `match`", start) + t else expr1Rest(postfixExpr(), location) - } + end expr1 - def expr1Rest(t: Tree, location: Location.Value): Tree = in.token match { + def expr1Rest(t: Tree, location: Location.Value): Tree = in.token match case EQUALS => - t match { - case Ident(_) | Select(_, _) | Apply(_, _) => - atSpan(startOffset(t), in.skipToken()) { Assign(t, subExpr()) } - case _ => - t - } + t match + case Ident(_) | Select(_, _) | Apply(_, _) => + atSpan(startOffset(t), in.skipToken()) { Assign(t, subExpr()) } + case _ => + t case COLON => in.nextToken() - val t1 = ascription(t, location) - if (in.token == MATCH) expr1Rest(t1, location) else t1 - case MATCH => - matchExpr(t, startOffset(t), Match) + ascription(t, location) case _ => t - } + end expr1Rest def ascription(t: Tree, location: Location.Value): Tree = atSpan(startOffset(t)) { in.token match { @@ -1951,20 +1945,20 @@ object Parsers { mkIf(cond, thenp, elsep) } - /** `match' (`{' CaseClauses `}' | CaseClause) + /** MatchClause ::= `match' `{' CaseClauses `}' */ - def matchExpr(t: Tree, start: Offset, mkMatch: (Tree, List[CaseDef]) => Match) = + def matchClause(t: Tree): Match = in.endMarkerScope(MATCH) { - atSpan(start, in.skipToken()) { - mkMatch(t, casesExpr(caseClause)) + atSpan(t.span.start, in.skipToken()) { + Match(t, inBracesOrIndented(caseClauses(caseClause))) } } - /** `match' (`{' TypeCaseClauses `}' | TypeCaseClause) + /** `match' `{' TypeCaseClauses `}' */ def matchType(t: Tree): MatchTypeTree = atSpan(t.span.start, accept(MATCH)) { - MatchTypeTree(EmptyTree, t, casesExpr(typeCaseClause)) + MatchTypeTree(EmptyTree, t, inBracesOrIndented(caseClauses(typeCaseClause))) } /** FunParams ::= Bindings @@ -2032,7 +2026,7 @@ object Parsers { /** PostfixExpr ::= InfixExpr [id [nl]] * InfixExpr ::= PrefixExpr * | InfixExpr id [nl] InfixExpr - * | InfixExpr ‘given’ (InfixExpr | ParArgumentExprs) + * | InfixExpr MatchClause */ def postfixExpr(): Tree = postfixExprRest(prefixExpr()) @@ -2063,6 +2057,7 @@ object Parsers { * | Path * | `(' [ExprsInParens] `)' * | SimpleExpr `.' id + * | SimpleExpr `.' MatchClause * | SimpleExpr (TypeArgs | NamedTypeArgs) * | SimpleExpr1 ArgumentExprs * Quoted ::= ‘'’ ‘{’ Block ‘}’ @@ -2395,10 +2390,6 @@ object Parsers { } } - def casesExpr(clause: () => CaseDef): List[CaseDef] = - if in.token == CASE then clause() :: Nil - else inBracesOrIndented(caseClauses(clause)) - /** CaseClauses ::= CaseClause {CaseClause} * TypeCaseClauses ::= TypeCaseClause {TypeCaseClause} */ diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 5238dce9b313..b57a4d636667 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -525,7 +525,8 @@ object Scanners { else if (lastWidth != nextWidth) errorButContinue(spaceTabMismatchMsg(lastWidth, nextWidth)) currentRegion match { - case Indented(curWidth, others, prefix, outer) if curWidth < nextWidth && !others.contains(nextWidth) => + case Indented(curWidth, others, prefix, outer) + if curWidth < nextWidth && !others.contains(nextWidth) && nextWidth != lastWidth => if (token == OUTDENT) errorButContinue( i"""The start of this line does not match any of the previous indentation widths. diff --git a/docs/docs/internals/syntax.md b/docs/docs/internals/syntax.md index 481f5e78dfe8..c4784d5d4e4e 100644 --- a/docs/docs/internals/syntax.md +++ b/docs/docs/internals/syntax.md @@ -147,8 +147,7 @@ FunArgTypes ::= InfixType | ‘(’ [ ‘[given]’ FunArgType {‘,’ FunArgType } ] ‘)’ | ‘(’ ‘[given]’ TypedFunParam {‘,’ TypedFunParam } ‘)’ TypedFunParam ::= id ‘:’ Type -MatchType ::= InfixType `match` - (‘{’ TypeCaseClauses ‘}’ | TypeCaseClause) +MatchType ::= InfixType `match` ‘{’ TypeCaseClauses ‘}’ InfixType ::= RefinedType {id [nl] RefinedType} InfixOp(t1, op, t2) RefinedType ::= WithType {[nl | ‘with’] Refinement} RefinedTypeTree(t, ds) WithType ::= AnnotType {‘with’ AnnotType} (deprecated) @@ -185,9 +184,8 @@ BlockResult ::= [‘implicit’] FunParams ‘=>’ Block FunParams ::= Bindings | id | ‘_’ -Expr1 ::= ‘if’ ‘(’ Expr ‘)’ {nl} - Expr [[semi] ‘else’ Expr] If(Parens(cond), thenp, elsep?) - | ‘if’ Expr ‘then’ Expr [[semi] ‘else’ Expr] If(cond, thenp, elsep?) +Expr1 ::= [‘inline’] ‘if’ ‘(’ Expr ‘)’ {nl} Expr [[semi] ‘else’ Expr] If(Parens(cond), thenp, elsep?) + | [‘inline’] ‘if’ Expr ‘then’ Expr [[semi] ‘else’ Expr] If(cond, thenp, elsep?) | ‘while’ ‘(’ Expr ‘)’ {nl} Expr WhileDo(Parens(cond), body) | ‘while’ Expr ‘do’ Expr WhileDo(cond, body) | ‘try’ Expr Catches [‘finally’ Expr] Try(expr, catches, expr?) @@ -198,17 +196,16 @@ Expr1 ::= ‘if’ ‘(’ Expr ‘)’ {nl} | HkTypeParamClause ‘=>’ Expr PolyFunction(ts, expr) | [SimpleExpr ‘.’] id ‘=’ Expr Assign(expr, expr) | SimpleExpr1 ArgumentExprs ‘=’ Expr Assign(expr, expr) - | Expr2 - | [‘inline’] Expr2 ‘match’ - (‘{’ CaseClauses ‘}’ | CaseClause) Match(expr, cases) -- point on match -Expr2 ::= PostfixExpr [Ascription] + | PostfixExpr [Ascription] + | ‘inline’ InfixExpr MatchClause Ascription ::= ‘:’ InfixType Typed(expr, tp) | ‘:’ Annotation {Annotation} Typed(expr, Annotated(EmptyTree, annot)*) Catches ::= ‘catch’ (Expr | CaseClause) PostfixExpr ::= InfixExpr [id] PostfixOp(expr, op) InfixExpr ::= PrefixExpr | InfixExpr id [nl] InfixExpr InfixOp(expr, op, expr) - | InfixExpr ‘given’ (InfixExpr | ParArgumentExprs) + | InfixExpr MatchClause +MatchClause ::= ‘match’ ‘{’ CaseClauses ‘}’ Match(expr, cases) PrefixExpr ::= [‘-’ | ‘+’ | ‘~’ | ‘!’] SimpleExpr PrefixOp(expr, op) SimpleExpr ::= Path | Literal @@ -221,6 +218,7 @@ SimpleExpr ::= Path | ‘new’ TemplateBody | ‘(’ ExprsInParens ‘)’ Parens(exprs) | SimpleExpr ‘.’ id Select(expr, id) + | SimpleExpr ‘.’ MatchClause | SimpleExpr TypeArgs TypeApply(expr, args) | SimpleExpr ArgumentExprs Apply(expr, args) | SimpleExpr ‘_’ PostfixOp(expr, _) diff --git a/docs/docs/reference/changed-features/match-syntax.md b/docs/docs/reference/changed-features/match-syntax.md new file mode 100644 index 000000000000..6841d922dbd1 --- /dev/null +++ b/docs/docs/reference/changed-features/match-syntax.md @@ -0,0 +1,43 @@ +--- +layout: doc-page +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 + } + + 2. `match` may follow a period: + + ```scala + if xsDefined + && 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 + +The new syntax of match expressions is as follows. +``` +InfixExpr ::= ... + | InfixExpr MatchClause +SimpleExpr ::= ... + | SimpleExpr ‘.’ MatchClause +MatchClause ::= ‘match’ ‘{’ CaseClauses ‘}’ +``` diff --git a/docs/docs/reference/contextual/typeclasses.md b/docs/docs/reference/contextual/typeclasses.md index 11c042a49c8f..01b2fd048a32 100644 --- a/docs/docs/reference/contextual/typeclasses.md +++ b/docs/docs/reference/contextual/typeclasses.md @@ -10,25 +10,22 @@ with canonical implementations defined by given instances. Here are some example ### Semigroups and monoids: ```scala -trait SemiGroup[T] { +trait SemiGroup[T] with def (x: T) combine (y: T): T -} -trait Monoid[T] extends SemiGroup[T] { + +trait Monoid[T] extends SemiGroup[T] with def unit: T -} -object Monoid { + +object Monoid with def apply[T](given Monoid[T]) = summon[Monoid[T]] -} -given Monoid[String] { +given Monoid[String] with def (x: String) combine (y: String): String = x.concat(y) def unit: String = "" -} -given Monoid[Int] { +given Monoid[Int] with def (x: Int) combine (y: Int): Int = x + y def unit: Int = 0 -} def sum[T: Monoid](xs: List[T]): T = xs.foldLeft(Monoid[T].unit)(_.combine(_)) diff --git a/docs/sidebar.yml b/docs/sidebar.yml index a41c87e4f420..d7a909e9dd31 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -127,6 +127,8 @@ sidebar: url: docs/reference/changed-features/implicit-conversions.html - title: Overload Resolution url: docs/reference/changed-features/overload-resolution.html + - title: Match Expressions + url: docs/reference/changed-features/match-syntax.html - title: Vararg Patterns url: docs/reference/changed-features/vararg-patterns.html - title: Pattern Bindings diff --git a/tests/neg/cannot-reduce-inline-match.check b/tests/neg/cannot-reduce-inline-match.check index 8f063f0003d2..adeab2fc97ee 100644 --- a/tests/neg/cannot-reduce-inline-match.check +++ b/tests/neg/cannot-reduce-inline-match.check @@ -1,11 +1,11 @@ -- Error: tests/neg/cannot-reduce-inline-match.scala:3:13 -------------------------------------------------------------- 3 | inline x match { // error - | ^ - | cannot reduce inline match with - | scrutinee: { - | "f" - | } : String("f") - | patterns : case _:Int + | ^ + | cannot reduce inline match with + | scrutinee: { + | "f" + | } : String("f") + | patterns : case _:Int | This location is in code that was inlined at cannot-reduce-inline-match.scala:9 4 | case _: Int => 5 | } diff --git a/tests/pos-special/fatal-warnings/unchecked-scrutinee.scala b/tests/pos-special/fatal-warnings/unchecked-scrutinee.scala index a51a833f93da..cde3d2ff5f91 100644 --- a/tests/pos-special/fatal-warnings/unchecked-scrutinee.scala +++ b/tests/pos-special/fatal-warnings/unchecked-scrutinee.scala @@ -1,5 +1,5 @@ object Test { - List(1: @unchecked, 2, 3): @unchecked match { + (List(1: @unchecked, 2, 3): @unchecked) match { case a :: as => } } \ No newline at end of file diff --git a/tests/pos/i6997c.scala b/tests/pos/i6997c.scala index 7702b5e998c9..9a9b481b335b 100644 --- a/tests/pos/i6997c.scala +++ b/tests/pos/i6997c.scala @@ -4,11 +4,11 @@ import scala.quoted.{_, given}, scala.quoted.matching._ inline def mcr(x: => Any): Any = ${mcrImpl('x)} -def mcrImpl(body: Expr[Any])(given ctx: QuoteContext): Expr[Any] = { - body match case '{$x: $t} => - '{ - val tmp: $t = $x - println(tmp) - tmp - } -} +def mcrImpl(body: Expr[Any])(given ctx: QuoteContext): Expr[Any] = + body match + case '{$x: $t} => + '{ + val tmp: $t = $x + println(tmp) + tmp + } diff --git a/tests/pos/matches.scala b/tests/pos/matches.scala new file mode 100644 index 000000000000..7f42addbe491 --- /dev/null +++ b/tests/pos/matches.scala @@ -0,0 +1,20 @@ +package matches +object Test with + 2 min 3 match + case 2 => "OK" + case 3 => "?" + match + case "OK" => + case "?" => assertFail() + + val x = 4 + if 2 < 3 + || x.match + case 4 => true + case _ => false + || (2 + 2).match + case 4 => true + case _ => false + then + println("ok") +end Test \ No newline at end of file diff --git a/tests/pos/multi-given.scala b/tests/pos/multi-given.scala index e171e03da2f2..b3d8e59626c1 100644 --- a/tests/pos/multi-given.scala +++ b/tests/pos/multi-given.scala @@ -4,3 +4,5 @@ trait C def fancy(given a: A, b: B, c: C) = "Fancy!" def foo(implicit a: A, b: B, c: C) = "foo" + +given A, B {} \ No newline at end of file diff --git a/tests/pos/single-case.scala b/tests/pos/single-case.scala index 3701bdc3e7f8..82466ec31733 100644 --- a/tests/pos/single-case.scala +++ b/tests/pos/single-case.scala @@ -1,11 +1,7 @@ object test with - type T[X] = X match case Int => X - try println("hi") catch case ex: java.io.IOException => println("ho") - 1 match case x: Int => println(x) - diff --git a/tests/run-macros/expr-map-2/Macro_1.scala b/tests/run-macros/expr-map-2/Macro_1.scala index ede05f1f9f87..2cbb2297746e 100644 --- a/tests/run-macros/expr-map-2/Macro_1.scala +++ b/tests/run-macros/expr-map-2/Macro_1.scala @@ -10,7 +10,8 @@ private object StringRewriter extends util.ExprMap { def transform[T](e: Expr[T])(given QuoteContext, Type[T]): Expr[T] = e match case '{ ($x: Foo).x } => - '{ new Foo(4).x } match case '{ $e: T } => e + '{ new Foo(4).x } match + case '{ $e: T } => e case _ => transformChildren(e) diff --git a/tests/run-with-compiler-custom-args/tasty-interpreter/interpreter/jvm/Interpreter.scala b/tests/run-with-compiler-custom-args/tasty-interpreter/interpreter/jvm/Interpreter.scala index 1a6aabd1b09a..84b5e582aff6 100644 --- a/tests/run-with-compiler-custom-args/tasty-interpreter/interpreter/jvm/Interpreter.scala +++ b/tests/run-with-compiler-custom-args/tasty-interpreter/interpreter/jvm/Interpreter.scala @@ -17,33 +17,32 @@ class Interpreter[R <: Reflection & Singleton](reflect0: R) extends TreeInterpre // Best effort to try to create a proxy val sym = fn.symbol.owner if (sym.isClassDef) { - sym.tree match case tree: ClassDef => - val parentSymbols = tree.parents.tail.map(_.asInstanceOf[TypeTree].symbol).head - import java.lang.reflect._ - val handler: InvocationHandler = new InvocationHandler() { - - def invoke(proxy: Object, method: Method, args: scala.Array[Object]): Object = { - if (LOG) { - val proxyString = if (method.getName == "toString") method.invoke(this) else proxy.toString - println(s"%> proxy call `$method` on `$proxyString` with args=${if (args == null) Nil else args.toList}") + sym.tree match + case tree: ClassDef => + val parentSymbols = tree.parents.tail.map(_.asInstanceOf[TypeTree].symbol).head + import java.lang.reflect._ + val handler: InvocationHandler = new InvocationHandler() { + def invoke(proxy: Object, method: Method, args: scala.Array[Object]): Object = { + if (LOG) { + val proxyString = if (method.getName == "toString") method.invoke(this) else proxy.toString + println(s"%> proxy call `$method` on `$proxyString` with args=${if (args == null) Nil else args.toList}") + } + + // println(method) + val symbol = sym.methods.find(_.name == method.getName).get + + if (symbol.isDefinedInCurrentRun) { + val argsList = if (args == null) Nil else args.toList + interpretCall(this, symbol, argsList).asInstanceOf[Object] + } + else { + assert(method.getClass == classOf[Object]) + method.invoke(this, args: _*) + } + } } - - // println(method) - val symbol = sym.methods.find(_.name == method.getName).get - - if (symbol.isDefinedInCurrentRun) { - val argsList = if (args == null) Nil else args.toList - interpretCall(this, symbol, argsList).asInstanceOf[Object] - } - else { - assert(method.getClass == classOf[Object]) - method.invoke(this, args: _*) - } - } - - } - val proxyClass: Class[_] = Proxy.getProxyClass(getClass.getClassLoader, jvmReflection.loadClass(parentSymbols.fullName)) - proxyClass.getConstructor(classOf[InvocationHandler]).newInstance(handler); + val proxyClass: Class[_] = Proxy.getProxyClass(getClass.getClassLoader, jvmReflection.loadClass(parentSymbols.fullName)) + proxyClass.getConstructor(classOf[InvocationHandler]).newInstance(handler); } } else jvmReflection.interpretNew(fn.symbol, evaluatedArgss(argss))