Skip to content
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

ScalametaParser: parse numbers with unary applied #3618

Merged
merged 2 commits into from Mar 10, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -544,7 +544,6 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
case Ident(x) => pred(x)
case _ => false
}
def isUnaryOp: Boolean = isIdentAnd(token, _.isUnaryOp)
def isIdentExcept(except: String) = isIdentAnd(token, _ != except)
def isIdentOf(tok: Token, name: String) = isIdentAnd(tok, _ == name)
@inline def isStar: Boolean = isStar(token)
Expand Down Expand Up @@ -1113,8 +1112,8 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
case _: Literal =>
if (dialect.allowLiteralTypes) literal()
else syntaxError(s"$dialect doesn't support literal types", at = path())
case Ident("-") if dialect.allowLiteralTypes && tryAhead[NumericConstant[_]] =>
numericLiteral(prevTokenPos, isNegated = true)
case Unary.Numeric(unary) if dialect.allowLiteralTypes && tryAhead[NumericConstant[_]] =>
numericLiteral(prevTokenPos, unary)
case _ => pathSimpleType()
}
simpleTypeRest(autoEndPosOpt(startPos)(res), startPos)
Expand Down Expand Up @@ -1332,29 +1331,30 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
if (acceptOpt[Dot]) selectors(name) else name
}

private def numericLiteral(startPos: Int, isNegated: Boolean): Lit = {
private def numericLiteral(startPos: Int, unary: Unary.Numeric): Lit = {
val number = token.asInstanceOf[NumericConstant[_]]
next()
autoEndPos(startPos)(numericLiteralAt(number, isNegated))
autoEndPos(startPos)(numericLiteralAt(number, unary))
}

private def numericLiteralAt(token: NumericConstant[_], isNegated: Boolean): Lit = {
private def numericLiteralAt(token: NumericConstant[_], unary: Unary.Numeric): Lit = {
def getBigInt(tok: NumericConstant[BigInt], dec: BigInt, hex: BigInt, typ: String) = {
// decimal never starts with `0` as octal was removed in 2.11; "hex" includes `0x` or `0b`
// non-decimal literals allow signed overflow within unsigned range
val max = if (tok.text(0) != '0') dec else hex
// token value is always positive as it doesn't take into account a sign
val value = tok.value
if (isNegated) {
if (value > max) syntaxError(s"integer number too small for $typ", at = token)
-value
val result = unary(value)
if (result.signum < 0) {
if (value > max) syntaxError(s"integer number too small for $typ", at = tok)
} else {
if (value >= max) syntaxError(s"integer number too large for $typ", at = token)
value
if (value >= max) syntaxError(s"integer number too large for $typ", at = tok)
}
result
}
def getBigDecimal(tok: NumericConstant[BigDecimal]) =
if (isNegated) -tok.value else tok.value
unary(tok.value)
.getOrElse(syntaxError(s"bad unary op `${unary.op}` for floating-point", at = tok))
token match {
case tok: Constant.Int =>
Lit.Int(getBigInt(tok, bigIntMaxInt, bigIntMaxUInt, "Int").intValue)
Expand All @@ -1369,7 +1369,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
}

def literal(): Lit = atCurPosNext(token match {
case number: NumericConstant[_] => numericLiteralAt(number, false)
case number: NumericConstant[_] => numericLiteralAt(number, Unary.Noop)
case Constant.Char(value) => Lit.Char(value)
case Constant.String(value) => Lit.String(value)
case t: Constant.Symbol =>
Expand Down Expand Up @@ -2228,25 +2228,30 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
}
}

def prefixExpr(allowRepeated: Boolean): Term =
if (!isUnaryOp) simpleExpr(allowRepeated)
else {
def prefixExpr(allowRepeated: Boolean): Term = token match {
case Unary((ident, unary)) =>
val startPos = tokenPos
val op = termName()
next()
def op = atPos(startPos)(Term.Name(ident))
def addPos(tree: Term) = autoEndPos(startPos)(tree)
def rest(tree: Term) = simpleExprRest(tree, canApply = true, startPos = startPos)
if (op.value == "-" && token.is[NumericConstant[_]])
rest(numericLiteral(startPos, isNegated = true))
else {
def otherwise =
simpleExpr0(allowRepeated = true) match {
case Success(result) => addPos(Term.ApplyUnary(op, result))
case Failure(_) =>
// maybe it is not unary operator but simply an ident `trait - {...}`
// we would fail here anyway, let's try to treat it as ident
rest(op)
}
(token, unary) match {
case (tok: NumericConstant[_], unary: Unary.Numeric) =>
next(); rest(addPos(numericLiteralAt(tok, unary)))
case (tok: BooleanConstant, unary: Unary.Logical) =>
next(); rest(addPos(Lit.Boolean(unary(tok.value))))
case _ => otherwise
}
}
case _ => simpleExpr(allowRepeated)
}

def simpleExpr(allowRepeated: Boolean): Term = simpleExpr0(allowRepeated).get

Expand Down Expand Up @@ -2910,11 +2915,10 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
autoEndPos(startPos)(token match {
case sidToken @ (_: Ident | _: KwThis | _: Unquote) =>
val sid = stableId()
if (token.is[NumericConstant[_]]) {
sid match {
case Term.Name("-") => return numericLiteral(startPos, isNegated = true)
case _ =>
}
(token, sidToken) match {
case (_: NumericConstant[_], Unary.Numeric(unary)) if prevTokenPos == startPos =>
return numericLiteral(startPos, unary)
case _ =>
}
val targs = if (token.is[LeftBracket]) Some(super.patternTypeArgs()) else None
if (token.is[LeftParen]) {
Expand Down
@@ -0,0 +1,63 @@
package scala.meta.internal.trees

import scala.meta.tokens.Token

private[meta] sealed trait Unary {
def op: String
}

private[meta] object Unary {

private val numericOpMap = Seq[Numeric](Plus, Minus, Tilde).map(x => x.op -> x).toMap
val opMap = numericOpMap ++ Seq[Unary](Not).map(x => x.op -> x)

def unapply(token: Token.Ident): Option[(String, Unary)] = {
val op = token.text
opMap.get(op).map(op -> _)
}

sealed trait Numeric extends Unary {
def apply(value: BigInt): BigInt
// could return None if not applicable (such as `~`)
def apply(value: BigDecimal): Option[BigDecimal]
}

object Numeric {
def unapply(token: Token.Ident): Option[Numeric] =
numericOpMap.get(token.text)
}

sealed trait Logical extends Unary {
def apply(value: Boolean): Boolean
}

case object Noop extends Numeric {
val op = ""
def apply(value: BigInt): BigInt = value
def apply(value: BigDecimal): Option[BigDecimal] = Some(value)
}

case object Plus extends Numeric {
val op = "+"
def apply(value: BigInt): BigInt = value
def apply(value: BigDecimal): Option[BigDecimal] = Some(value)
}

case object Minus extends Numeric {
val op = "-"
def apply(value: BigInt): BigInt = -value
def apply(value: BigDecimal): Option[BigDecimal] = Some(-value)
}

case object Tilde extends Numeric {
val op = "~"
def apply(value: BigInt): BigInt = ~value
def apply(value: BigDecimal): Option[BigDecimal] = None
}

case object Not extends Logical {
val op = "!"
def apply(value: Boolean): Boolean = !value
}

}
Expand Up @@ -43,7 +43,7 @@ package object trees {
// some heuristic is needed to govern associativity and precedence of unquoted operators
def isLeftAssoc: Boolean = value.last != ':'

def isUnaryOp: Boolean = Set("-", "+", "~", "!").contains(value)
def isUnaryOp: Boolean = Unary.opMap.contains(value)

def isAssignmentOp = value match {
case "!=" | "<=" | ">=" | "" => false
Expand Down
39 changes: 21 additions & 18 deletions tests/shared/src/test/scala/scala/meta/tests/parsers/LitSuite.scala
Expand Up @@ -271,9 +271,9 @@ class LitSuite extends ParseSuite {
}

test("unary: +1") {
runTestAssert[Stat]("+1")(Term.ApplyUnary(tname("+"), lit(1)))
val tree = Term.ApplyUnary(tname("+"), Term.Apply(lit(1), List(lit(0))))
runTestAssert[Stat]("+1(0)")(tree)
runTestAssert[Stat]("+1", "1")(lit(1))
val tree = Term.Apply(lit(1), List(lit(0)))
runTestAssert[Stat]("+1(0)", "1(0)")(tree)
}

test("unary: -1") {
Expand All @@ -283,9 +283,9 @@ class LitSuite extends ParseSuite {
}

test("unary: ~1") {
runTestAssert[Stat]("~1")(Term.ApplyUnary(tname("~"), lit(1)))
val tree = Term.ApplyUnary(tname("~"), Term.Apply(lit(1), List(lit(0))))
runTestAssert[Stat]("~1(0)")(tree)
runTestAssert[Stat]("~1", "-2")(lit(-2))
val tree = Term.Apply(lit(-2), List(lit(0)))
runTestAssert[Stat]("~1(0)", "-2(0)")(tree)
}

test("unary: !1") {
Expand All @@ -295,9 +295,9 @@ class LitSuite extends ParseSuite {
}

test("unary: +1.0") {
runTestAssert[Stat]("+1.0", "+1.0d")(Term.ApplyUnary(tname("+"), lit(1d)))
val tree = Term.ApplyUnary(tname("+"), Term.Apply(lit(1d), List(lit(0))))
runTestAssert[Stat]("+1.0(0)", "+1.0d(0)")(tree)
runTestAssert[Stat]("+1.0", "1.0d")(lit(1d))
val tree = Term.Apply(lit(1d), List(lit(0)))
runTestAssert[Stat]("+1.0(0)", "1.0d(0)")(tree)
}

test("unary: -1.0") {
Expand All @@ -307,9 +307,12 @@ class LitSuite extends ParseSuite {
}

test("unary: ~1.0") {
runTestAssert[Stat]("~1.0", "~1.0d")(Term.ApplyUnary(tname("~"), lit(1d)))
val tree = Term.ApplyUnary(tname("~"), Term.Apply(lit(1d), List(lit(0))))
runTestAssert[Stat]("~1.0(0)", "~1.0d(0)")(tree)
def error(code: String) =
s"""|<input>:1: error: bad unary op `~` for floating-point
|~$code
| ^""".stripMargin
runTestError[Stat]("~1.0", error("1.0"))
runTestError[Stat]("~1.0(0)", error("1.0(0)"))
}

test("unary: !1.0") {
Expand All @@ -319,15 +322,15 @@ class LitSuite extends ParseSuite {
}

test("unary: !true") {
runTestAssert[Stat]("!true")(Term.ApplyUnary(tname("!"), lit(true)))
val tree = Term.ApplyUnary(tname("!"), Term.Apply(lit(true), List(lit(0))))
runTestAssert[Stat]("!true(0)")(tree)
runTestAssert[Stat]("!true", "false")(lit(false))
val tree = Term.Apply(lit(false), List(lit(0)))
runTestAssert[Stat]("!true(0)", "false(0)")(tree)
}

test("unary: !false") {
runTestAssert[Stat]("!false")(Term.ApplyUnary(tname("!"), lit(false)))
val tree = Term.ApplyUnary(tname("!"), Term.Apply(lit(false), List(lit(0))))
runTestAssert[Stat]("!false(0)")(tree)
runTestAssert[Stat]("!false", "true")(lit(true))
val tree = Term.Apply(lit(true), List(lit(0)))
runTestAssert[Stat]("!false(0)", "true(0)")(tree)
}

test("scalatest-like infix without literal") {
Expand Down
Expand Up @@ -76,13 +76,13 @@ class TreeSyntaxSuite extends scala.meta.tests.parsers.ParseSuite {
testBlockAddNL("Foo.bar")
testBlockAddNL("10")
testBlockAddNL("-10")
testBlockAddNL("~10")
testBlockAddNL("~10", "-11")
testBlockAddNL("10.0d")
testBlockAddNL("-10.0d")
testBlockAddNL("true")
testBlockAddNL("false")
testBlockAddNL("!true")
testBlockAddNL("!false")
testBlockAddNL("!true", "false")
testBlockAddNL("!false", "true")
testBlockNoNL("-{10}", "-{\n 10\n}")
testBlockAddNL("foo(bar)")
testBlockAddNL("foo[Bar]")
Expand Down
19 changes: 19 additions & 0 deletions tests/shared/src/test/scala/scala/meta/tests/trees/TreeSuite.scala
Expand Up @@ -3,10 +3,29 @@ package trees

import munit._
import scala.meta._
import scala.meta.internal.trees._

class TreeSuite extends FunSuite {
test("Name.unapply") {
assert(Name.unapply(q"a").contains("a"))
assert(Name.unapply(t"a").contains("a"))
}

Seq(
("+", Unary.Plus),
("-", Unary.Minus),
("~", Unary.Tilde),
("!", Unary.Not)
).foreach { case (op, unary) =>
test(s"Unary.$unary") {
assertEquals(unary.op, op)
assertEquals(Unary.opMap.get(op), Some(unary))
assert(op.isUnaryOp)
}
}

test(s"Unary opMap size") {
assertEquals(Unary.opMap.size, 4)
}

}