Skip to content

Commit

Permalink
Permit indent at width after colon arrow eol
Browse files Browse the repository at this point in the history
  • Loading branch information
som-snytt committed Jan 29, 2025
1 parent 9cb97ec commit 5d4028e
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 10 deletions.
5 changes: 5 additions & 0 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1090,6 +1090,7 @@ object Parsers {
def isArrowIndent() =
lookahead.isArrow
&& {
lookahead.observeArrowEOL()
lookahead.nextToken()
lookahead.token == INDENT || lookahead.token == EOF
}
Expand Down Expand Up @@ -2654,10 +2655,14 @@ object Parsers {

def closureRest(start: Int, location: Location, params: List[Tree]): Tree =
atSpan(start, in.offset) {
if location == Location.InColonArg then
in.observeArrowEOL()
if in.token == CTXARROW then
if params.isEmpty then
syntaxError(em"context function literals require at least one formal parameter", Span(start, in.lastOffset))
in.nextToken()
else if in.token == ARROWeol then
in.nextToken()
else
accept(ARROW)
val body =
Expand Down
30 changes: 22 additions & 8 deletions compiler/src/dotty/tools/dotc/parsing/Scanners.scala
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ object Scanners {
|| token == IDENTIFIER && isOperatorPart(name(name.length - 1))

def isArrow =
token == ARROW || token == CTXARROW
token == ARROW || token == CTXARROW || token == ARROWeol
}

abstract class ScannerCommon(source: SourceFile)(using Context) extends CharArrayReader with TokenData {
Expand Down Expand Up @@ -612,7 +612,11 @@ object Scanners {
insert(if (pastBlankLine) NEWLINES else NEWLINE, lineOffset)
else if indentIsSignificant then
if nextWidth < lastWidth
|| nextWidth == lastWidth && (indentPrefix == MATCH || indentPrefix == CATCH) && token != CASE then
|| nextWidth == lastWidth
&& indentPrefix.match
case MATCH | CATCH => token != CASE
case _ => false
then
if currentRegion.isOutermost then
if nextWidth < lastWidth then currentRegion = topLevelRegion(nextWidth)
else if !isLeadingInfixOperator(nextWidth) && !statCtdTokens.contains(lastToken) && lastToken != INDENT then
Expand All @@ -637,9 +641,13 @@ object Scanners {
insert(OUTDENT, offset)
else if r.isInstanceOf[InBraces] && !closingRegionTokens.contains(token) then
report.warning("Line is indented too far to the left, or a `}` is missing", sourcePos())

else if lastWidth < nextWidth
|| lastWidth == nextWidth && (lastToken == MATCH || lastToken == CATCH) && token == CASE then
|| lastWidth == nextWidth
&& lastToken.match
case MATCH | CATCH => token == CASE
case ARROWeol => true
case _ => false
then
if canStartIndentTokens.contains(lastToken) then
currentRegion = Indented(nextWidth, lastToken, currentRegion)
insert(INDENT, offset)
Expand All @@ -657,7 +665,7 @@ object Scanners {
def spaceTabMismatchMsg(lastWidth: IndentWidth, nextWidth: IndentWidth): Message =
em"""Incompatible combinations of tabs and spaces in indentation prefixes.
|Previous indent : $lastWidth
|Latest indent : $nextWidth"""
|Latest indent : $nextWidth"""

def observeColonEOL(inTemplate: Boolean): Unit =
val enabled =
Expand All @@ -671,6 +679,13 @@ object Scanners {
reset()
if atEOL then token = COLONeol

def observeArrowEOL(): Unit =
if indentSyntax && token == ARROW then
peekAhead()
val atEOL = isAfterLineEnd || token == EOF
reset()
if atEOL then token = ARROWeol

def observeIndented(): Unit =
if indentSyntax && isNewLine then
val nextWidth = indentWidth(next.offset)
Expand All @@ -679,7 +694,6 @@ object Scanners {
currentRegion = Indented(nextWidth, COLONeol, currentRegion)
offset = next.offset
token = INDENT
end observeIndented

/** Insert an <outdent> token if next token closes an indentation region.
* Exception: continue if indentation region belongs to a `match` and next token is `case`.
Expand Down Expand Up @@ -1099,7 +1113,7 @@ object Scanners {
reset()
next

class LookaheadScanner(val allowIndent: Boolean = false) extends Scanner(source, offset, allowIndent = allowIndent) {
class LookaheadScanner(allowIndent: Boolean = false) extends Scanner(source, offset, allowIndent = allowIndent) {
override protected def initialCharBufferSize = 8
override def languageImportContext = Scanner.this.languageImportContext
}
Expand Down Expand Up @@ -1651,7 +1665,7 @@ object Scanners {
case class InCase(outer: Region) extends Region(OUTDENT)

/** A class describing an indentation region.
* @param width The principal indendation width
* @param width The principal indentation width
* @param prefix The token before the initial <indent> of the region
*/
case class Indented(width: IndentWidth, prefix: Token, outer: Region | Null) extends Region(OUTDENT):
Expand Down
5 changes: 3 additions & 2 deletions compiler/src/dotty/tools/dotc/parsing/Tokens.scala
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,10 @@ object Tokens extends TokensCommon {
inline val COLONeol = 89; enter(COLONeol, ":", ": at eol")
// A `:` recognized as starting an indentation block
inline val SELFARROW = 90; enter(SELFARROW, "=>") // reclassified ARROW following self-type
inline val ARROWeol = 99; enter(ARROWeol, "=>", "=> at eol") // lambda ARROW at eol followed by indent

/** XML mode */
inline val XMLSTART = 99; enter(XMLSTART, "$XMLSTART$<") // TODO: deprecate
inline val XMLSTART = 100; enter(XMLSTART, "$XMLSTART$<") // TODO: deprecate

final val alphaKeywords: TokenSet = tokenRange(IF, END)
final val symbolicKeywords: TokenSet = tokenRange(USCORE, CTXARROW)
Expand Down Expand Up @@ -282,7 +283,7 @@ object Tokens extends TokensCommon {
final val closingRegionTokens = BitSet(RBRACE, RPAREN, RBRACKET, CASE) | statCtdTokens

final val canStartIndentTokens: BitSet =
statCtdTokens | BitSet(COLONeol, WITH, EQUALS, ARROW, CTXARROW, LARROW, WHILE, TRY, FOR, IF, THROW, RETURN)
statCtdTokens | BitSet(COLONeol, WITH, EQUALS, ARROWeol, ARROW, CTXARROW, LARROW, WHILE, TRY, FOR, IF, THROW, RETURN)

/** Faced with the choice between a type and a formal parameter, the following
* tokens determine it's a formal parameter.
Expand Down
44 changes: 44 additions & 0 deletions tests/neg/i22193.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@

def fn2(arg: String, arg2: String)(f: String => Unit): Unit = f(arg)

def fn3(arg: String, arg2: String)(f: => Unit): Unit = f

def test1() =

fn2(arg = "blue sleeps faster than tuesday", arg2 = "the quick brown fox jumped over the lazy dog"): env =>
val x = env
println(x)

fn2( // error not a legal formal parameter for a function literal
arg = "blue sleeps faster than tuesday",
arg2 = "the quick brown fox jumped over the lazy dog"): env =>
val x = env // error
println(x)

fn2( // error
arg = "blue sleeps faster than tuesday",
arg2 = "the quick brown fox jumped over the lazy dog"): env =>
val x = env // error
println(x)

fn2(
arg = "blue sleeps faster than tuesday",
arg2 = "the quick brown fox jumped over the lazy dog"):
env => // error indented definitions expected, identifier env found
val x = env
println(x)

def test2() =

fn2(
arg = "blue sleeps faster than tuesday",
arg2 = "the quick brown fox jumped over the lazy dog"
): env =>
val x = env
println(x)

fn3( // error missing argument list for value of type (=> Unit) => Unit
arg = "blue sleeps faster than tuesday",
arg2 = "the quick brown fox jumped over the lazy dog"):
val x = "Hello" // error
println(x) // error
57 changes: 57 additions & 0 deletions tests/pos/i22193.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@

def fn2(arg: String, arg2: String)(f: String => Unit): Unit = f(arg)

def fn3(arg: String, arg2: String)(f: => Unit): Unit = f

def test() =

fn2(arg = "blue sleeps faster than tuesday", arg2 = "the quick brown fox jumped over the lazy dog"): env =>
val x = env
println(x)

// doesn't compile
fn2(
arg = "blue sleeps faster than tuesday",
arg2 = "the quick brown fox jumped over the lazy dog"): env =>
val x = env
println(x)

fn2(
arg = "blue sleeps faster than tuesday",
arg2 = "the quick brown fox jumped over the lazy dog"): env =>
val x = env
println(x)

// does compile
fn2(
arg = "blue sleeps faster than tuesday",
arg2 = "the quick brown fox jumped over the lazy dog"):
env =>
val x = env
println(x)

// does compile
fn2(
arg = "blue sleeps faster than tuesday",
arg2 = "the quick brown fox jumped over the lazy dog"
): env =>
val x = env
println(x)

fn3(
arg = "blue sleeps faster than tuesday",
arg2 = "the quick brown fox jumped over the lazy dog"):
val x = "Hello"
println(x)

fn3(
arg = "blue sleeps faster than tuesday",
arg2 = "the quick brown fox jumped over the lazy dog"):
val x = "Hello"
println(x)

// don't turn innocent empty cases into functions
def regress(x: Int) =
x match
case 42 =>
case _ =>

0 comments on commit 5d4028e

Please sign in to comment.