diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index b6e3604516e0..9e0e48374e2b 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -74,7 +74,6 @@ object StdNames { final val IFkw: N = kw("if") final val IMPLICITkw: N = kw("implicit") final val IMPORTkw: N = kw("import") - final val INLINEkw: N = kw("inline") final val LAZYkw: N = kw("lazy") final val MACROkw: N = kw("macro") final val MATCHkw: N = kw("match") @@ -436,6 +435,7 @@ object StdNames { val implicitConversions: N = "implicitConversions" val implicitly: N = "implicitly" val in: N = "in" + val inline: N = "inline" val info: N = "info" val inlinedEquals: N = "inlinedEquals" val internal: N = "internal" @@ -478,6 +478,7 @@ object StdNames { val notify_ : N = "notify" val null_ : N = "null" val ofDim: N = "ofDim" + val opaque: N = "opaque" val origin: N = "origin" val prefix : N = "prefix" val productArity: N = "productArity" diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 24d3192beebf..79bc1d46b5dd 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -166,22 +166,27 @@ object Parsers { def isSimpleLiteral: Boolean = simpleLiteralTokens contains in.token def isLiteral: Boolean = literalTokens contains in.token def isNumericLit: Boolean = numericLitTokens contains in.token - def isModifier: Boolean = modifierTokens.contains(in.token) || isIdent(nme.INLINEkw) - def isBindingIntro: Boolean = canStartBindingTokens contains in.token def isTemplateIntro: Boolean = templateIntroTokens contains in.token def isDclIntro: Boolean = dclIntroTokens contains in.token def isStatSeqEnd: Boolean = in.token == RBRACE || in.token == EOF def mustStartStat: Boolean = mustStartStatTokens contains in.token + /** Is current token a hard or soft modifier (in modifier position or not)? */ + def isModifier: Boolean = modifierTokens.contains(in.token) || in.isSoftModifier + + def isBindingIntro: Boolean = + canStartBindingTokens.contains(in.token) && + !in.isSoftModifierInModifierPosition + def isExprIntro: Boolean = - (canStartExpressionTokens `contains` in.token) && - (!isIdent(nme.INLINEkw) || lookaheadIn(canStartExpressionTokens)) + canStartExpressionTokens.contains(in.token) && + !in.isSoftModifierInModifierPosition def isDefIntro(allowedMods: BitSet): Boolean = in.token == AT || (defIntroTokens `contains` in.token) || (allowedMods `contains` in.token) || - isIdent(nme.INLINEkw) && lookaheadIn(BitSet(AT) | defIntroTokens | allowedMods) + in.isSoftModifierInModifierPosition def isStatSep: Boolean = in.token == NEWLINE || in.token == NEWLINES || in.token == SEMI @@ -455,14 +460,6 @@ object Parsers { def commaSeparated[T](part: () => T): List[T] = tokenSeparated(COMMA, part) - /** Is the token following the current one in `tokens`? */ - def lookaheadIn(tokens: BitSet): Boolean = { - val lookahead = in.lookaheadScanner - do lookahead.nextToken() - while (lookahead.token == NEWLINE || lookahead.token == NEWLINES) - tokens.contains(lookahead.token) - } - /* --------- OPERAND/OPERATOR STACK --------------------------------------- */ var opStack: List[OpInfo] = Nil @@ -841,7 +838,7 @@ object Parsers { /** Is current ident a `*`, and is it followed by a `)` or `,`? */ def isPostfixStar: Boolean = - in.name == nme.raw.STAR && lookaheadIn(BitSet(RPAREN, COMMA)) + in.name == nme.raw.STAR && in.lookaheadIn(BitSet(RPAREN, COMMA)) def infixTypeRest(t: Tree): Tree = infixOps(t, canStartTypeTokens, refinedType, isType = true, isOperator = !isPostfixStar) @@ -899,7 +896,7 @@ object Parsers { val start = in.skipToken() typeBounds().withPos(Position(start, in.lastOffset, start)) } - else if (isIdent(nme.raw.TILDE) && lookaheadIn(BitSet(IDENTIFIER, BACKQUOTED_IDENT))) + else if (isIdent(nme.raw.TILDE) && in.lookaheadIn(BitSet(IDENTIFIER, BACKQUOTED_IDENT))) atPos(in.offset) { PrefixOp(typeIdent(), path(thisOK = true)) } else path(thisOK = false, handleSingletonType) match { case r @ SingletonTypeTree(_) => r @@ -1744,8 +1741,11 @@ object Parsers { case PRIVATE => Mod.Private() case PROTECTED => Mod.Protected() case SEALED => Mod.Sealed() - case OPAQUE => Mod.Opaque() - case IDENTIFIER if name == nme.INLINEkw => Mod.Inline() + case IDENTIFIER => + name match { + case nme.inline => Mod.Inline() + case nme.opaque => Mod.Opaque() + } } /** Drop `private' modifier when followed by a qualifier. @@ -1816,7 +1816,8 @@ object Parsers { @tailrec def loop(mods: Modifiers): Modifiers = { if (allowed.contains(in.token) || - isIdent(nme.INLINEkw) && localModifierTokens.subsetOf(allowed)) { + in.isSoftModifier && + localModifierTokens.subsetOf(allowed)) { // soft modifiers are admissible everywhere local modifiers are val isAccessMod = accessModifierTokens contains in.token val mods1 = addModifier(mods) loop(if (isAccessMod) accessQualifierOpt(mods1) else mods1) @@ -1957,7 +1958,8 @@ object Parsers { } } else { - if (isIdent(nme.INLINEkw)) mods = addModifier(mods) + if (isIdent(nme.inline) && in.isSoftModifierInParamModifierPosition) + mods = addModifier(mods) mods = atPos(start) { mods | Param } } atPos(start, nameStart) { @@ -2616,7 +2618,7 @@ object Parsers { if (in.token == IMPLICIT || in.token == ERASED) { val start = in.offset var imods = modifiers(funArgMods) - if (isBindingIntro && !isIdent(nme.INLINEkw)) + if (isBindingIntro) stats += implicitClosure(start, Location.InBlock, imods) else stats +++= localDef(start, imods) diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 63f907af6b5c..7dc3f19bfb32 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -11,7 +11,7 @@ import util.NameTransformer.avoidIllegalChars import Tokens._ import scala.annotation.{ switch, tailrec } import scala.collection.mutable -import scala.collection.immutable.SortedMap +import scala.collection.immutable.{SortedMap, BitSet} import rewrites.Rewrites.patch object Scanners { @@ -301,9 +301,6 @@ object Scanners { case _ => } - /** A new Scanner that starts at the current token offset */ - def lookaheadScanner: Scanner = new Scanner(source, offset) - /** Produce next token, filling TokenData fields of Scanner. */ def nextToken(): Unit = { @@ -637,6 +634,28 @@ object Scanners { else false } +// Lookahead --------------------------------------------------------------- + + /** A new Scanner that starts at the current token offset */ + def lookaheadScanner: Scanner = new Scanner(source, offset) + + /** Is the token following the current one in `tokens`? */ + def lookaheadIn(tokens: BitSet): Boolean = { + val lookahead = lookaheadScanner + do lookahead.nextToken() + while (lookahead.token == NEWLINE || lookahead.token == NEWLINES) + tokens.contains(lookahead.token) + } + + /** Is the current token in a position where a modifier is allowed? */ + def inModifierPosition(): Boolean = { + val lookahead = lookaheadScanner + do lookahead.nextToken() + while (lookahead.token == NEWLINE || lookahead.token == NEWLINES || + lookahead.isSoftModifier) + modifierFollowers.contains(lookahead.token) + } + // Identifiers --------------------------------------------------------------- private def getBackquotedIdent(): Unit = { @@ -717,6 +736,14 @@ object Scanners { } } + def isSoftModifier: Boolean = + token == IDENTIFIER && softModifierNames.contains(name) + + def isSoftModifierInModifierPosition: Boolean = + isSoftModifier && inModifierPosition() + + def isSoftModifierInParamModifierPosition: Boolean = + isSoftModifier && !lookaheadIn(BitSet(COLON)) // Literals ----------------------------------------------------------------- diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index 6f425e53dd3e..1d1ded5e13b9 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -4,6 +4,7 @@ package parsing import collection.immutable.BitSet import core.Decorators._ +import core.StdNames.nme abstract class TokensCommon { def maxToken: Int @@ -93,7 +94,6 @@ abstract class TokensCommon { //final val FORSOME = 61; enter(FORSOME, "forSome") // TODO: deprecate //final val ENUM = 62; enter(ENUM, "enum") //final val ERASED = 63; enter(ERASED, "erased") - //final val OPAQUE = 64; enter(OPAQUE, "opaque") /** special symbols */ final val COMMA = 70; enter(COMMA, "','") @@ -178,7 +178,6 @@ object Tokens extends TokensCommon { final val FORSOME = 61; enter(FORSOME, "forSome") // TODO: deprecate final val ENUM = 62; enter(ENUM, "enum") final val ERASED = 63; enter(ERASED, "erased") - final val OPAQUE = 64; enter(OPAQUE, "opaque") /** special symbols */ final val NEWLINE = 78; enter(NEWLINE, "end of statement", "new line") @@ -199,7 +198,7 @@ object Tokens extends TokensCommon { /** XML mode */ final val XMLSTART = 96; enter(XMLSTART, "$XMLSTART$<") // TODO: deprecate - final val alphaKeywords: TokenSet = tokenRange(IF, OPAQUE) + final val alphaKeywords: TokenSet = tokenRange(IF, ERASED) final val symbolicKeywords: TokenSet = tokenRange(USCORE, VIEWBOUND) final val symbolicTokens: TokenSet = tokenRange(COMMA, VIEWBOUND) final val keywords: TokenSet = alphaKeywords | symbolicKeywords @@ -227,7 +226,7 @@ object Tokens extends TokensCommon { final val defIntroTokens: TokenSet = templateIntroTokens | dclIntroTokens final val localModifierTokens: TokenSet = BitSet( - ABSTRACT, FINAL, SEALED, IMPLICIT, LAZY, ERASED, OPAQUE) + ABSTRACT, FINAL, SEALED, IMPLICIT, LAZY, ERASED) final val accessModifierTokens: TokenSet = BitSet( PRIVATE, PROTECTED) @@ -237,6 +236,8 @@ object Tokens extends TokensCommon { final val modifierTokensOrCase: TokenSet = modifierTokens | BitSet(CASE) + final val modifierFollowers = modifierTokens | defIntroTokens + /** Is token only legal as start of statement (eof also included)? */ final val mustStartStatTokens: TokenSet = defIntroTokens | modifierTokens | BitSet(IMPORT, PACKAGE) @@ -247,4 +248,6 @@ object Tokens extends TokensCommon { TYPE, RPAREN, RBRACE, RBRACKET) final val numericLitTokens: TokenSet = BitSet(INTLIT, LONGLIT, FLOATLIT, DOUBLELIT) + + final val softModifierNames = Set(nme.`inline`, nme.`opaque`) } diff --git a/compiler/src/dotty/tools/dotc/printing/SyntaxHighlighting.scala b/compiler/src/dotty/tools/dotc/printing/SyntaxHighlighting.scala index 58d1cc3bbcec..989a3804e23a 100644 --- a/compiler/src/dotty/tools/dotc/printing/SyntaxHighlighting.scala +++ b/compiler/src/dotty/tools/dotc/printing/SyntaxHighlighting.scala @@ -53,6 +53,7 @@ object SyntaxHighlighting { val start = scanner.offset val token = scanner.token val name = scanner.name + val isSoftModifier = scanner.isSoftModifierInModifierPosition scanner.nextToken() val end = scanner.lastOffset @@ -67,11 +68,7 @@ object SyntaxHighlighting { // we don't highlight it, hence the `-1` highlightRange(start, end - 1, LiteralColor) - case _ if alphaKeywords.contains(token) => - highlightRange(start, end, KeywordColor) - - case IDENTIFIER if name == nme.INLINEkw => - // `inline` is a "soft" keyword + case _ if alphaKeywords.contains(token) || isSoftModifier => highlightRange(start, end, KeywordColor) case IDENTIFIER if name == nme.??? => diff --git a/compiler/test/dotty/tools/dotc/printing/SyntaxHighlightingTests.scala b/compiler/test/dotty/tools/dotc/printing/SyntaxHighlightingTests.scala index c53a2cd773e9..8bff254e663d 100644 --- a/compiler/test/dotty/tools/dotc/printing/SyntaxHighlightingTests.scala +++ b/compiler/test/dotty/tools/dotc/printing/SyntaxHighlightingTests.scala @@ -125,5 +125,8 @@ class SyntaxHighlightingTests extends DottyTest { test("inline def foo = 1", " = ") test("@inline def foo = 1", " = ") test("class inline", " ") + test("val inline = 2", " = ") + test("def inline = 2", " = ") + test("def foo(inline: Int) = 2", " (: ) = ") } } diff --git a/docs/docs/reference/inline.md b/docs/docs/reference/inline.md index aac7c914ca25..8f5a9928b3c0 100644 --- a/docs/docs/reference/inline.md +++ b/docs/docs/reference/inline.md @@ -3,8 +3,7 @@ layout: doc-page title: Inline --- -`inline` is a new modifier that guarantees that a definition will be -inline at the point of use. Example: +`inline` is a new [soft modifier](./soft-modifier.html) that guarantees that a definition will be inline at the point of use. Example: object Config { inline val logging = false diff --git a/docs/docs/reference/opaques.md b/docs/docs/reference/opaques.md index e4a2867049ef..b347250cc2ab 100644 --- a/docs/docs/reference/opaques.md +++ b/docs/docs/reference/opaques.md @@ -53,4 +53,6 @@ But the following operations would lead to type errors: l / l2 // error: `/` is not a member fo Logarithm ``` +`opaque` is a [soft modifier](./soft-modifier.html). + For more details, see [Scala SIP 35](https://docs.scala-lang.org/sips/opaque-types.html). \ No newline at end of file diff --git a/docs/docs/reference/soft-modifier.md b/docs/docs/reference/soft-modifier.md new file mode 100644 index 000000000000..524ef1a40536 --- /dev/null +++ b/docs/docs/reference/soft-modifier.md @@ -0,0 +1,11 @@ +--- +layout: doc-page +title: Soft Modifiers +--- + +A soft modifier is one of the identifiers `opaque` and `inline`. + +It is treated as a potential modifier of a definition, if it is followed by a hard modifier or a keyword combination starting a definition (`def`, `val`, `var`, `type`, `class`, `case class`, `trait`, `object`, `case object`, `enum`). Between the two words there may be a sequence of newline tokens and soft modifiers. + +It is treated as a potential modifier of a parameter binding unless it is followed by `:`. + diff --git a/tests/neg/inlinevals.scala b/tests/neg/inlinevals.scala index 513283e63581..45d073850eaa 100644 --- a/tests/neg/inlinevals.scala +++ b/tests/neg/inlinevals.scala @@ -7,7 +7,7 @@ object Test { inline val N = 10 def X = 20 - inline inline val twice = 30 // error: repeated modifier // error: not found: inline + inline inline val twice = 30 // error: repeated modifier class C(inline x: Int, private inline val y: Int) { // error // error inline val foo: Int // error: abstract member may not be inline diff --git a/tests/pos/i5145.scala b/tests/pos/i5145.scala new file mode 100644 index 000000000000..f978ac7b71c4 --- /dev/null +++ b/tests/pos/i5145.scala @@ -0,0 +1,16 @@ +class Test { + def foo(x: Int): Int = { + val inline = 3 + def opaque(x: Int): Unit = () + opaque(3) + inline + } + def bar(inline: Int => Int) = 3 + inline def baz(inline x: Int => Int) = 3 + + locally { + bar(inline = identity) + bar(inline => inline) + bar(implicit inline => inline) + } +}