From 0b0436af02ead4cb3c3f9e9773519661424088f8 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Thu, 9 Oct 2025 08:57:50 -0700 Subject: [PATCH 1/6] Inline fetchLeadingZero in getNumber --- .../dotty/tools/dotc/parsing/Scanners.scala | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 52e03de60dea..02850a83a24b 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -163,9 +163,9 @@ object Scanners { strVal = litBuf.toString litBuf.clear() - @inline def isNumberSeparator(c: Char): Boolean = c == '_' + inline def isNumberSeparator(c: Char): Boolean = c == '_' - @inline def removeNumberSeparators(s: String): String = if (s.indexOf('_') == -1) s else s.replace("_", "") + def removeNumberSeparators(s: String): String = if (s.indexOf('_') == -1) s else s.replace("_", "") // disallow trailing numeric separator char, but continue lexing def checkNoTrailingSeparator(): Unit = @@ -916,21 +916,7 @@ object Scanners { putChar('/') getOperatorRest() } - case '0' => - def fetchLeadingZero(): Unit = { - nextChar() - ch match { - case 'x' | 'X' => base = 16 ; nextChar() - case 'b' | 'B' => base = 2 ; nextChar() - case _ => base = 10 ; putChar('0') - } - if (base != 10 && !isNumberSeparator(ch) && digit2int(ch, base) < 0) - error(em"invalid literal number") - } - fetchLeadingZero() - getNumber() - case '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => - base = 10 + case '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => getNumber() case '`' => getBackquotedIdent() @@ -1500,9 +1486,21 @@ object Scanners { if (isIdentifierPart(ch) && ch >= ' ') error(em"Invalid literal number") - /** Read a number into strVal and set base - */ - protected def getNumber(): Unit = { + /** Read a number into strVal and set base and token. + */ + def getNumber(): Unit = + def checkNumberChar() = + if !isNumberSeparator(ch) && digit2int(ch, base) < 0 then + error(em"invalid literal number") + if ch == '0' then + nextChar() + ch match + case 'x' | 'X' => base = 16; nextChar(); checkNumberChar() + case 'b' | 'B' => base = 2; nextChar(); checkNumberChar() + case _ => base = 10; putChar('0') + else + base = 10 + while (isNumberSeparator(ch) || digit2int(ch, base) >= 0) { putChar(ch) nextChar() @@ -1525,11 +1523,9 @@ object Scanners { token = LONGLIT case _ => } - checkNoTrailingSeparator() - setStrVal() - } + end getNumber private def finishCharLit(): Unit = { nextChar() From c78cf745e2d2ee8408f032f70a1788b3c0a7c5ac Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Thu, 9 Oct 2025 11:57:21 -0700 Subject: [PATCH 2/6] Warn negative literal parsing under -Wsyntax --- .../src/dotty/tools/dotc/config/ScalaSettings.scala | 2 ++ compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 6 +++++- .../dotty/tools/dotc/reporting/ErrorMessageID.scala | 2 ++ .../src/dotty/tools/dotc/reporting/messages.scala | 12 ++++++++++++ tests/warn/unary-minus.scala | 13 +++++++++++++ 5 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 tests/warn/unary-minus.scala diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 61b7b981f529..6b0da5faed52 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -169,6 +169,7 @@ private sealed trait WarningSettings: private val WtoStringInterpolated = BooleanSetting(WarningSetting, "Wtostring-interpolated", "Warn a standard interpolator used toString on a reference type.") private val WrecurseWithDefault = BooleanSetting(WarningSetting, "Wrecurse-with-default", "Warn when a method calls itself with a default argument.") private val WwrongArrow = BooleanSetting(WarningSetting, "Wwrong-arrow", "Warn if function arrow was used instead of context literal ?=>.") + private val Wsyntax = BooleanSetting(WarningSetting, "Wsyntax", "Warn if code relies on misleading syntax.") private val Wunused: Setting[List[ChoiceWithHelp[String]]] = MultiChoiceHelpSetting( WarningSetting, name = "Wunused", @@ -308,6 +309,7 @@ private sealed trait WarningSettings: def recurseWithDefault(using Context): Boolean = allOr(WrecurseWithDefault) def wrongArrow(using Context): Boolean = allOr(WwrongArrow) def safeInit(using Context): Boolean = allOr(WsafeInit) + def syntax(using Context): Boolean = allOr(Wsyntax) /** -X "Extended" or "Advanced" settings */ private sealed trait XSettings: diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 64eb442c239a..1199d9297b14 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1361,8 +1361,10 @@ object Parsers { def literalOf(token: Token): Tree = { val isNegated = negOffset < in.offset def digits0 = in.removeNumberSeparators(in.strVal) - def digits = if (isNegated) "-" + digits0 else digits0 + def digits = if isNegated then "-" + digits0 else digits0 if !inTypeOrSingleton then + if isNegated && negOffset < in.offset - 1 && ctx.settings.Whas.syntax then + warning(DetachedUnaryMinus(), negOffset) token match { case INTLIT => return Number(digits, NumberKind.Whole(in.base)) case DECILIT => return Number(digits, NumberKind.Decimal) @@ -2826,6 +2828,8 @@ object Parsers { if (canApply) argumentStart() in.token match case DOT => + if ctx.settings.Whas.syntax then + t match { case Number(n, _) if n(0) == '-' => warning(UnaryMinusInSelect(), t.span.start) case _ => } in.nextToken() simpleExprRest(selectorOrMatch(t), location, canApply = true) case LBRACKET => diff --git a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala index 103687abdbff..f04320b8494b 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -235,6 +235,8 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe case CannotInstantiateQuotedTypeVarID // errorNumber: 219 case DefaultShadowsGivenID // errorNumber: 220 case RecurseWithDefaultID // errorNumber: 221 + case DetachedUnaryMinusID // errorNumber: 222 + case UnaryMinusInSelectID // errorNumber: 223 def errorNumber = ordinal - 1 diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 9c4ee9926bf2..8c36e3345711 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -3741,3 +3741,15 @@ final class RecurseWithDefault(name: Name)(using Context) extends TypeMsg(Recurs i"Recursive call used a default argument for parameter $name." override protected def explain(using Context): String = "It's more explicit to pass current or modified arguments in a recursion." + +final class DetachedUnaryMinus()(using Context) extends SyntaxMsg(DetachedUnaryMinusID): + override protected def msg(using Context): String = + "Unary minus is too far to the left." + override protected def explain(using Context): String = + "The expression is parsed as a literal, and intervening space is ignored." + +final class UnaryMinusInSelect()(using Context) extends SyntaxMsg(UnaryMinusInSelectID): + override protected def msg(using Context): String = + "Negative literal should be parenthesized before dot selection." + override protected def explain(using Context): String = + "The receiver is parsed as a negative literal, not as the negation of the whole expression." diff --git a/tests/warn/unary-minus.scala b/tests/warn/unary-minus.scala new file mode 100644 index 000000000000..54eee6d64d51 --- /dev/null +++ b/tests/warn/unary-minus.scala @@ -0,0 +1,13 @@ +//> using options -Wsyntax -Wnonunit-statement + +class C { + def f1 = -2.abs // warn funky precedence + def f2 = - 2.abs // warn meaningless space + def f3 = - 2 // warn meaningless space + def f4 = 42 + -2.abs // warn precedence // hides warn unused expression + def f5 = 42 + - 2.abs // nowarn infix + def f6 = (-2).abs // nowarn explicit precedence + def f7 = -3.14 // nowarn decimal point +} From bcd0b0519e2a38b260eaf9d760d2e784fe4ffffc Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Thu, 9 Oct 2025 12:41:49 -0700 Subject: [PATCH 3/6] Call it start in literal --- .../dotty/tools/dotc/parsing/Parsers.scala | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 1199d9297b14..69e89d5da304 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1345,7 +1345,7 @@ object Parsers { if isIdent(nme.raw.MINUS) then val start = in.offset in.nextToken() - literal(negOffset = start, inTypeOrSingleton = true) + literal(start, inTypeOrSingleton = true) else literal(inTypeOrSingleton = true) @@ -1354,17 +1354,17 @@ object Parsers { * | symbolLiteral * | ‘null’ * - * @param negOffset The offset of a preceding `-' sign, if any. - * If the literal is not negated, negOffset == in.offset. + * @param start The offset of a preceding `-' sign, if any. + * If the literal is not negated, start == in.offset. */ - def literal(negOffset: Int = in.offset, inPattern: Boolean = false, inTypeOrSingleton: Boolean = false, inStringInterpolation: Boolean = false): Tree = { + def literal(start: Int = in.offset, inPattern: Boolean = false, inTypeOrSingleton: Boolean = false, inStringInterpolation: Boolean = false): Tree = { def literalOf(token: Token): Tree = { - val isNegated = negOffset < in.offset + val isNegated = start < in.offset def digits0 = in.removeNumberSeparators(in.strVal) def digits = if isNegated then "-" + digits0 else digits0 if !inTypeOrSingleton then - if isNegated && negOffset < in.offset - 1 && ctx.settings.Whas.syntax then - warning(DetachedUnaryMinus(), negOffset) + if isNegated && start < in.offset - 1 && ctx.settings.Whas.syntax then + warning(DetachedUnaryMinus(), start) token match { case INTLIT => return Number(digits, NumberKind.Whole(in.base)) case DECILIT => return Number(digits, NumberKind.Decimal) @@ -1397,15 +1397,15 @@ object Parsers { val t = in.token match { case STRINGLIT | STRINGPART => val value = in.strVal - atSpan(negOffset, negOffset, negOffset + value.length) { Literal(Constant(value)) } + atSpan(start, start, start + value.length) { Literal(Constant(value)) } case _ => syntaxErrorOrIncomplete(IllegalLiteral()) - atSpan(negOffset) { Literal(Constant(null)) } + atSpan(start) { Literal(Constant(null)) } } in.nextToken() t } - else atSpan(negOffset) { + else atSpan(start) { if (in.token == QUOTEID) if ((staged & StageKind.Spliced) != 0 && Chars.isIdentifierStart(in.name(0))) { val t = atSpan(in.offset + 1) { @@ -1478,7 +1478,7 @@ object Parsers { nextSegment(in.offset + offsetCorrection) offsetCorrection = 0 if (in.token == STRINGLIT) - segmentBuf += literal(inPattern = inPattern, negOffset = in.offset + offsetCorrection, inStringInterpolation = true) + segmentBuf += literal(in.offset + offsetCorrection, inPattern = inPattern, inStringInterpolation = true) InterpolatedString(interpolator, segmentBuf.toList) } From 0a3f97ba2a25d290cf49ceadca9bfe8fa071e43c Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Thu, 9 Oct 2025 15:26:52 -0700 Subject: [PATCH 4/6] Use isNegatedNumber to exclude backtick minus --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 10 ++++------ tests/neg/i24162.scala | 5 +++++ 2 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 tests/neg/i24162.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 69e89d5da304..c521ad739ef6 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -219,9 +219,8 @@ object Parsers { isIdent(nme.erased) && in.erasedEnabled && in.isSoftModifierInParamModifierPosition def isConsume = isIdent(nme.consume) && ccEnabled //\&& in.isSoftModifierInParamModifierPosition - def isSimpleLiteral = - simpleLiteralTokens.contains(in.token) - || isIdent(nme.raw.MINUS) && numericLitTokens.contains(in.lookahead.token) + def isNegatedNumber = isIdent(nme.raw.MINUS) && numericLitTokens.contains(in.lookahead.token) + def isSimpleLiteral = simpleLiteralTokens.contains(in.token) || isNegatedNumber def isLiteral = literalTokens contains in.token def isNumericLit = numericLitTokens contains in.token def isTemplateIntro = templateIntroTokens contains in.token @@ -3304,9 +3303,8 @@ object Parsers { */ def simplePattern(): Tree = in.token match { case IDENTIFIER | BACKQUOTED_IDENT | THIS | SUPER => - simpleRef() match - case id @ Ident(nme.raw.MINUS) if isNumericLit => literal(startOffset(id)) - case t => simplePatternRest(t) + if isNegatedNumber then literal(start = in.skipToken()) + else simplePatternRest(simpleRef()) case USCORE => wildcardIdent() case LPAREN => diff --git a/tests/neg/i24162.scala b/tests/neg/i24162.scala new file mode 100644 index 000000000000..22d97ce99521 --- /dev/null +++ b/tests/neg/i24162.scala @@ -0,0 +1,5 @@ + +def test(x: Int) = + x match + case `-`42 => true // error => expected + case _ => false // error unindent expected, case found From 895398d94a17ce56a0ff209d9558bf5a02b5ecac Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Thu, 9 Oct 2025 17:40:27 -0700 Subject: [PATCH 5/6] Tighten condition in prefixExpr --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 13 +++++++------ tests/pos/i7910.scala | 13 +++++++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 tests/pos/i7910.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index c521ad739ef6..eb4fdb581388 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2744,14 +2744,15 @@ object Parsers { */ val prefixExpr: Location => Tree = location => if in.token == IDENTIFIER && nme.raw.isUnary(in.name) - && in.canStartExprTokens.contains(in.lookahead.token) + && { + val lookahead = in.lookahead + in.canStartExprTokens.contains(lookahead.token) && lookahead.lineOffset < 0 + } then - val start = in.offset - val op = termIdent() - if (op.name == nme.raw.MINUS && isNumericLit) - simpleExprRest(literal(start), location, canApply = true) + if isNegatedNumber then + simpleExprRest(literal(start = in.skipToken()), location, canApply = true) else - atSpan(start) { PrefixOp(op, simpleExpr(location)) } + atSpan(in.offset) { PrefixOp(termIdent(), simpleExpr(location)) } else simpleExpr(location) /** SimpleExpr ::= ‘new’ ConstrApp {`with` ConstrApp} [TemplateBody] diff --git a/tests/pos/i7910.scala b/tests/pos/i7910.scala new file mode 100644 index 000000000000..5ffaa30cc2d3 --- /dev/null +++ b/tests/pos/i7910.scala @@ -0,0 +1,13 @@ + +class C { + def k = { + object + extends Function1[Int, Int] { def apply(i: Int): Int = i + 1 } + val g: Int => Int = + + g(1) + } + def ok = { + val i = 42 + val n = +i + n + } +} From 9826103274739afc9b8514ae0248d5a7509a4c4b Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Thu, 9 Oct 2025 18:00:40 -0700 Subject: [PATCH 6/6] Retain simpleLiteral --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index eb4fdb581388..36e5c25650a9 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1342,9 +1342,7 @@ object Parsers { */ def simpleLiteral(): Tree = if isIdent(nme.raw.MINUS) then - val start = in.offset - in.nextToken() - literal(start, inTypeOrSingleton = true) + literal(start = in.skipToken(), inTypeOrSingleton = true) else literal(inTypeOrSingleton = true)