From ab999342567d43dfbbeb387765d3b51a96a1228e Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Mon, 3 Nov 2025 20:33:41 -0800 Subject: [PATCH 1/2] Permit new with trivial end --- compiler/src/dotty/tools/dotc/ast/Trees.scala | 6 +- .../dotty/tools/dotc/parsing/Parsers.scala | 63 ++++++++++++------- tests/pos/i24250.scala | 31 +++++++++ 3 files changed, 77 insertions(+), 23 deletions(-) create mode 100644 tests/pos/i24250.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Trees.scala b/compiler/src/dotty/tools/dotc/ast/Trees.scala index ef64a025805a..56a58d358b15 100644 --- a/compiler/src/dotty/tools/dotc/ast/Trees.scala +++ b/compiler/src/dotty/tools/dotc/ast/Trees.scala @@ -323,6 +323,8 @@ object Trees { import WithEndMarker.* + /** The span of the name in the end marker, or `NoSpan` if none. + */ final def endSpan(using Context): Span = if hasEndMarker then val realName = srcName.stripModuleClassSuffix.lastPart @@ -333,8 +335,8 @@ object Trees { /** The name in source code that represents this construct, * and is the name that the user must write to create a valid * end marker. - * e.g. a constructor definition is terminated in the source - * code by `end this`, so it's `srcName` should return `this`. + * E.g., a constructor definition is terminated in the source + * code by `end this`, so its `srcName` should return `this`. */ protected def srcName(using Context): Name diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 147a650370ac..92db016db6f8 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -349,43 +349,45 @@ object Parsers { if in.isNewLine then in.nextToken() else accept(SEMI) /** Parse statement separators and end markers. Ensure that there is at least - * one statement separator unless the next token terminates a statement´sequence. - * @param stats the statements parsed to far + * one statement separator unless the next token terminates a statement sequence. + * @param stats the statements parsed so far * @param noPrevStat true if there was no immediately preceding statement parsed * @param what a string indicating what kind of statement is parsed * @param altEnd a token that is also considered as a terminator of the statement - * sequence (the default `EOF` already assumes to terminate a statement + * sequence (the default `EOF` is already assumed to terminate a statement * sequence). * @return true if the statement sequence continues, false if it terminates. */ def statSepOrEnd[T <: Tree](stats: ListBuffer[T], noPrevStat: Boolean = false, what: String = "statement", altEnd: Token = EOF): Boolean = + inline def stopping = false + inline def continuing = true def recur(sepSeen: Boolean, endSeen: Boolean): Boolean = if isStatSep then in.nextToken() - recur(true, endSeen) + recur(sepSeen = true, endSeen) else if in.token == END then if endSeen then syntaxError(em"duplicate end marker") checkEndMarker(stats) recur(sepSeen, endSeen = true) else if isStatSeqEnd || in.token == altEnd then - false + stopping else if sepSeen || endSeen then - true + continuing else val found = in.token val statFollows = mustStartStatTokens.contains(found) syntaxError( if noPrevStat then IllegalStartOfStatement(what, isModifier, statFollows) else em"end of $what expected but ${showToken(found)} found") - if mustStartStatTokens.contains(found) then - false // it's a statement that might be legal in an outer context + if statFollows then + stopping // it's a statement that might be legal in an outer context else in.nextToken() // needed to ensure progress; otherwise we might cycle forever skip() - true + continuing in.observeOutdented() - recur(false, false) + recur(sepSeen = false, endSeen = false) end statSepOrEnd def rewriteNotice(version: SourceVersion = `3.0-migration`, additionalOption: String = "") = @@ -1566,15 +1568,23 @@ object Parsers { if MigrationVersion.Scala2to3.needsPatch then patch(source, Span(in.offset), " ") - def possibleTemplateStart(isNew: Boolean = false): Unit = + def possibleTemplateStart(): Unit = + possiblyNewTemplateStart(): Unit + + /** Return true on trivial end */ + def possiblyNewTemplateStart(): Boolean = in.observeColonEOL(inTemplate = true) if in.token == COLONeol then - if in.lookahead.token == END then in.token = NEWLINE + if in.lookahead.token == END then + in.token = NEWLINE + true else in.nextToken() if in.token != LBRACE then acceptIndent() + false else newLineOptWhenFollowedBy(LBRACE) + false def checkEndMarker[T <: Tree](stats: ListBuffer[T]): Unit = @@ -1599,6 +1609,10 @@ object Parsers { case _: ParsedTry => in.token == TRY case _: Match => in.token == MATCH case _: New => in.token == NEW + case _: Apply => + methPart(stat) match + case Select(_: New, nme.CONSTRUCTOR) => in.token == NEW + case _ => false case _: (ForYield | ForDo) => in.token == FOR case _ => false @@ -2915,15 +2929,22 @@ object Parsers { val parents = if in.isNestedStart then Nil else constrApps(exclude = COMMA) - possibleTemplateStart(isNew = true) - parents match { - case parent :: Nil if !in.isNestedStart => - reposition(if (parent.isType) ensureApplied(wrapNew(parent)) else parent) - case tkn if in.token == INDENT => - New(templateBodyOpt(emptyConstructor, parents, Nil)) - case _ => - New(reposition(templateBodyOpt(emptyConstructor, parents, Nil))) - } + val colonized = possiblyNewTemplateStart() + parents match + case parent :: Nil if !in.isNestedStart => + reposition: + if colonized then New(Template(emptyConstructor, parents, derived = Nil, self = EmptyValDef, body = Nil)) + else if parent.isType then ensureApplied(wrapNew(parent)) + else parent + case parents => + // With brace syntax, the last token consumed by a parser is }, but with indent syntax, + // the last token consumed by a parser is OUTDENT, which causes mismatching spans, so don't reposition. + val indented = in.token == INDENT + val body = + val bo = templateBodyOpt(emptyConstructor, parents, derived = Nil) + if !indented then reposition(bo) else bo + New(body) + end newExpr /** ExprsInParens ::= ExprInParens {`,' ExprInParens} * | NamedExprInParens {‘,’ NamedExprInParens} diff --git a/tests/pos/i24250.scala b/tests/pos/i24250.scala new file mode 100644 index 000000000000..218349742e38 --- /dev/null +++ b/tests/pos/i24250.scala @@ -0,0 +1,31 @@ + +trait Foo +val foo = + new Foo: + // comment + end new +val foo2 = + new Foo: + end new +val foo3 = + new Foo { + } + end new + +class C +val c = + new C: + end new +val cc = + new C + end new +val ccc = + new C { + } + +class D: +end D +val d = + new D: + def more = ??? + end new From dc504855d6af128d6622ac3ba44e9ee840c91b0b Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Mon, 3 Nov 2025 23:12:55 -0800 Subject: [PATCH 2/2] Drop end new expression --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 4 ---- tests/neg/i24250.scala | 5 +++++ tests/pos/i24250.scala | 5 +---- 3 files changed, 6 insertions(+), 8 deletions(-) create mode 100644 tests/neg/i24250.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 92db016db6f8..450bbf17ddeb 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1609,10 +1609,6 @@ object Parsers { case _: ParsedTry => in.token == TRY case _: Match => in.token == MATCH case _: New => in.token == NEW - case _: Apply => - methPart(stat) match - case Select(_: New, nme.CONSTRUCTOR) => in.token == NEW - case _ => false case _: (ForYield | ForDo) => in.token == FOR case _ => false diff --git a/tests/neg/i24250.scala b/tests/neg/i24250.scala new file mode 100644 index 000000000000..a668e0ec6162 --- /dev/null +++ b/tests/neg/i24250.scala @@ -0,0 +1,5 @@ + +class C +val cc = + new C + end new // error diff --git a/tests/pos/i24250.scala b/tests/pos/i24250.scala index 218349742e38..0a2fd569b69c 100644 --- a/tests/pos/i24250.scala +++ b/tests/pos/i24250.scala @@ -16,10 +16,7 @@ class C val c = new C: end new -val cc = - new C - end new -val ccc = +val c2 = new C { }