Skip to content

Allow nested end marker scopes at same indentation width #7571

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 16 additions & 23 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1278,15 +1278,8 @@ object Parsers {
if silentTemplateIndent && !isNew then in.observeIndented()
newLineOptWhenFollowedBy(LBRACE)

def indentRegion[T](tag: EndMarkerTag)(op: => T): T = {
val iw = in.currentRegion.indentWidth
val t = op
in.consumeEndMarker(tag, iw)
t
}

def indentRegion[T](pid: Tree)(op: => T): T = pid match {
case pid: RefTree => indentRegion(pid.name.toTermName)(op)
def endMarkerScope[T](pid: Tree)(op: => T): T = pid match {
case pid: RefTree => in.endMarkerScope(pid.name.toTermName)(op)
case _ => op
}

Expand Down Expand Up @@ -1789,9 +1782,9 @@ object Parsers {

def expr1(location: Location.Value = Location.ElseWhere): Tree = in.token match {
case IF =>
indentRegion(IF) { ifExpr(in.offset, If) }
in.endMarkerScope(IF) { ifExpr(in.offset, If) }
case WHILE =>
indentRegion(WHILE) {
in.endMarkerScope(WHILE) {
atSpan(in.skipToken()) {
val cond = condExpr(DO)
newLinesOpt()
Expand Down Expand Up @@ -1826,7 +1819,7 @@ object Parsers {
WhileDo(Block(body, cond), Literal(Constant(())))
}
case TRY =>
indentRegion(TRY) {
in.endMarkerScope(TRY) {
val tryOffset = in.offset
atSpan(in.skipToken()) {
val body = expr()
Expand Down Expand Up @@ -1961,7 +1954,7 @@ object Parsers {
/** `match' (`{' CaseClauses `}' | CaseClause)
*/
def matchExpr(t: Tree, start: Offset, mkMatch: (Tree, List[CaseDef]) => Match) =
indentRegion(MATCH) {
in.endMarkerScope(MATCH) {
atSpan(start, in.skipToken()) {
mkMatch(t, casesExpr(caseClause))
}
Expand Down Expand Up @@ -2157,7 +2150,7 @@ object Parsers {
* | ‘new’ TemplateBody
*/
def newExpr(): Tree =
indentRegion(NEW) {
in.endMarkerScope(NEW) {
val start = in.skipToken()
def reposition(t: Tree) = t.withSpan(Span(start, in.lastOffset))
possibleBracesStart()
Expand Down Expand Up @@ -2323,7 +2316,7 @@ object Parsers {
* {nl} [`yield'] Expr
* | `for' Enumerators (`do' Expr | `yield' Expr)
*/
def forExpr(): Tree = indentRegion(FOR) {
def forExpr(): Tree = in.endMarkerScope(FOR) {
atSpan(in.skipToken()) {
var wrappedEnums = true
val start = in.offset
Expand Down Expand Up @@ -3074,7 +3067,7 @@ object Parsers {
else emptyType
val rhs =
if (tpt.isEmpty || in.token == EQUALS)
indentRegion(first) {
endMarkerScope(first) {
accept(EQUALS)
if (in.token == USCORE && !tpt.isEmpty && mods.is(Mutable) &&
(lhs.toList forall (_.isInstanceOf[Ident])))
Expand Down Expand Up @@ -3166,7 +3159,7 @@ object Parsers {
if (in.isScala2CompatMode) newLineOptWhenFollowedBy(LBRACE)
val rhs =
if (in.token == EQUALS)
indentRegion(name) {
in.endMarkerScope(name) {
in.nextToken()
subExpr()
}
Expand Down Expand Up @@ -3298,7 +3291,7 @@ object Parsers {
}

def classDefRest(start: Offset, mods: Modifiers, name: TypeName): TypeDef =
indentRegion(name.toTermName) {
in.endMarkerScope(name.toTermName) {
val constr = classConstr(isCaseClass = mods.is(Case))
val templ = templateOpt(constr)
finalizeDef(TypeDef(name, templ), mods, start)
Expand All @@ -3322,7 +3315,7 @@ object Parsers {
*/
def objectDef(start: Offset, mods: Modifiers): ModuleDef = atSpan(start, nameStart) {
val name = ident()
indentRegion(name) {
in.endMarkerScope(name) {
val templ = templateOpt(emptyConstructor)
finalizeDef(ModuleDef(name, templ), mods, start)
}
Expand All @@ -3332,7 +3325,7 @@ object Parsers {
*/
def enumDef(start: Offset, mods: Modifiers): TypeDef = atSpan(start, nameStart) {
val modulName = ident()
indentRegion(modulName) {
in.endMarkerScope(modulName) {
val clsName = modulName.toTypeName
val constr = classConstr()
val templ = template(constr, isEnum = true)
Expand Down Expand Up @@ -3412,7 +3405,7 @@ object Parsers {
else
(EmptyTermName, isIdent(nme.extension))

val gdef = indentRegion(if name.isEmpty then GIVEN else name) {
val gdef = in.endMarkerScope(if name.isEmpty then GIVEN else name) {
if isExtension then
if (in.token == COLON) in.nextToken()
assert(ident() == nme.extension)
Expand Down Expand Up @@ -3597,7 +3590,7 @@ object Parsers {
*/
def packaging(start: Int): Tree = {
val pkg = qualId()
indentRegion(pkg) {
endMarkerScope(pkg) {
possibleTemplateStart()
val stats = inDefScopeBraces(topStatSeq())
makePackaging(start, pkg, stats)
Expand Down Expand Up @@ -3785,7 +3778,7 @@ object Parsers {
else
val pkg = qualId()
var continue = false
indentRegion(pkg) {
endMarkerScope(pkg) {
possibleTemplateStart()
if in.token == EOF then
ts += makePackaging(start, pkg, List())
Expand Down
81 changes: 36 additions & 45 deletions compiler/src/dotty/tools/dotc/parsing/Scanners.scala
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,8 @@ object Scanners {
/** The current region. This is initially an Indented region with indentation width. */
var currentRegion: Region = Indented(IndentWidth.Zero, Set(), EMPTY, null)

/** The end marker that was skipped last */
val endMarkers = new mutable.ListBuffer[EndMarker]
/** The number of open end marker scopes */
var openEndMarkers: List[(EndMarkerTag, IndentWidth)] = Nil

// Scala 2 compatibility

Expand Down Expand Up @@ -324,48 +324,47 @@ object Scanners {
this.token = token
}

/** If this token and the next constitute an end marker, skip them and append a new EndMarker
* value at the end of the endMarkers queue.
/** What can be referred to in an end marker */
type EndMarkerTag = TermName | Token

/** Establish a scope for a passible end-marker with given tag, parsed by `op` */
def endMarkerScope[T](tag: EndMarkerTag)(op: => T): T =
val saved = openEndMarkers
openEndMarkers = (tag, currentRegion.indentWidth) :: openEndMarkers
try op finally openEndMarkers = saved

/** If this token and the next constitute an end marker, skip them and check they
* align with an opening construct with the same end marker tag.
*/
private def handleEndMarkers(width: IndentWidth): Unit =
if (next.token == IDENTIFIER && next.name == nme.end && width == currentRegion.indentWidth) {
protected def skipEndMarker(width: IndentWidth): Unit =
if next.token == IDENTIFIER && next.name == nme.end then
val lookahead = LookaheadScanner()
lookahead.nextToken() // skip the `end`
val start = lookahead.offset

def handle(tag: EndMarkerTag) =
def checkAligned(): Unit = openEndMarkers match
case (etag, ewidth) :: rest if width <= ewidth =>
if width < ewidth || tag != etag then
openEndMarkers = rest
checkAligned()
case _ =>
lexical.println(i"misaligned end marker $tag, $width, $openEndMarkers")
errorButContinue("misaligned end marker", start)

def handle(tag: EndMarkerTag) = {
val skipTo = lookahead.charOffset
lookahead.nextToken()
if (lookahead.isAfterLineEnd || lookahead.token == EOF) {
lexical.println(i"produce end marker $tag $width")
endMarkers += EndMarker(tag, width, offset)
if lookahead.isAfterLineEnd || lookahead.token == EOF then
checkAligned()
next.token = EMPTY
while (charOffset < skipTo) nextChar()
}
}
while charOffset < skipTo do nextChar()
end handle

lookahead.token match {
lookahead.nextToken() // skip the `end`
lookahead.token match
case IDENTIFIER | BACKQUOTED_IDENT => handle(lookahead.name)
case IF | WHILE | FOR | MATCH | TRY | NEW | GIVEN => handle(lookahead.token)
case _ =>
}
}

/** Consume and cancel the head of the end markers queue if it has the given `tag` and width.
* Flag end markers with higher indent widths as errors.
*/
def consumeEndMarker(tag: EndMarkerTag, width: IndentWidth): Unit = {
lexical.println(i"consume end marker $tag $width")
if (endMarkers.nonEmpty) {
val em = endMarkers.head
if (width <= em.width) {
if (em.tag != tag || em.width != width) {
lexical.println(i"misaligned end marker ${em.tag}, ${em.width} at ${width}")
errorButContinue("misaligned end marker", em.offset)
}
endMarkers.trimStart(1)
}
}
}
end skipEndMarker

/** A leading symbolic or backquoted identifier is treated as an infix operator if
* - it does not follow a blank line, and
Expand Down Expand Up @@ -501,6 +500,7 @@ object Scanners {
&& !isLeadingInfixOperator()
then
insert(if (pastBlankLine) NEWLINES else NEWLINE, lineOffset)
skipEndMarker(nextWidth)
else if indentIsSignificant then
if nextWidth < lastWidth
|| nextWidth == lastWidth && (indentPrefix == MATCH || indentPrefix == CATCH) && token != CASE then
Expand All @@ -511,7 +511,7 @@ object Scanners {
case r: Indented =>
currentRegion = r.enclosing
insert(OUTDENT, offset)
handleEndMarkers(nextWidth)
skipEndMarker(nextWidth)
case r: InBraces if !closingRegionTokens.contains(token) =>
ctx.warning("Line is indented too far to the left, or a `}' is missing",
source.atSpan(Span(offset)))
Expand Down Expand Up @@ -883,6 +883,7 @@ object Scanners {

class LookaheadScanner(indent: Boolean = false) extends Scanner(source, offset) {
override val indentSyntax = indent
override def skipEndMarker(width: IndentWidth) = ()
override protected def printState() = {
print("la:")
super.printState()
Expand Down Expand Up @@ -1416,16 +1417,6 @@ object Scanners {
val Zero = Run(' ', 0)
}

/** What can be referred to in an end marker */
type EndMarkerTag = TermName | Token

/** A processed end marker
* @param tag The name or token referred to in the marker
* @param width The indentation width where the marker occurred
* @param offset The offset of the `end`
*/
case class EndMarker(tag: EndMarkerTag, width: IndentWidth, offset: Int)

// ------------- keyword configuration -----------------------------------

private val (lastKeywordStart, kwArray) = buildKeywordArray(keywords)
Expand Down
18 changes: 9 additions & 9 deletions tests/neg/endmarkers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ object Test
locally {
var x = 0
while x < 10 do x += 1
end while // error: end of statement expected but while found // error: not found: end
val f = 10 // error: ';' expected, but 'val' found
end while
val f = 10
while
x += 1
x < 10
do ()
end while // error: misaligned end marker // error: not found : end
} // error: ';' expected, but '}' found
end while // error: misaligned end marker
}

def f(x: Int): Int =
val y =
Expand Down Expand Up @@ -50,24 +50,24 @@ class Test2
object x
new Test2 {
override def foo = 2
end new // error: end of statement expected but new found // error: not found: end
} // error: ';' expected, but '}' found
end new // error: misaligned end marker
}
def bar = 2
end Test2 // error: misaligned end marker
end Test2

class Test3
self =>
def foo = 1
end Test3 // error: not found: end
end Test3 // error: misaligned end marker

import collection.mutable.HashMap

class Coder(words: List[String])

class Foo
println()
end Foo // error: not found: end
end Foo // error: misaligned end marker

(2 -> "ABC", new ArrowAssoc('3') -> "DEF")

Expand Down Expand Up @@ -101,4 +101,4 @@ class Coder(words: List[String])
.flatMap {
case (digit, str) => str map (ltr => ltr -> digit)
}
end Coder // error: The start of this line does not match any of the previous indentation widths.
end Coder // error: misaligned end marker
18 changes: 18 additions & 0 deletions tests/neg/endmarkers1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

def f7[T](x: Option[T]) = x match
case Some(y) =>
case None =>
end if // error: misaligned end marker

object Test4 with
def f[T](x: Option[T]) = x match
case Some(y) =>
case None =>
end if // error: misaligned end marker
end Test // error: misaligned end marker

def f[T](x: Option[T]) = x match
case Some(y) =>
case None => "hello"
end f // error: misaligned end marker

14 changes: 14 additions & 0 deletions tests/pos/end-nested.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
def f[T](x: Option[T]) = x match
case Some(y) =>
case None =>
end f

object Test with
try List(1, 2, 3) match
case x :: xs => println(x)
case Nil => println("Nil")
catch
case ex: java.io.IOException => println(ex)
case ex: Throwable => throw ex
end try