diff --git a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ExpressionParser.scala b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ExpressionParser.scala index 053fbdd0f..6454741af 100644 --- a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ExpressionParser.scala +++ b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ExpressionParser.scala @@ -83,6 +83,7 @@ private object ExpressionParser { ReferenceCallParser.parseOrFail | AnnotationParser.parseOrFail | TupleParser.parseOrFail | + NumberParser.parseOrFail | BooleanParser.parseOrFail | IdentifierParser.parseOrFail } diff --git a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/NumberParser.scala b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/NumberParser.scala new file mode 100644 index 000000000..7deeb72b9 --- /dev/null +++ b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/NumberParser.scala @@ -0,0 +1,46 @@ +package org.alephium.ralph.lsp.access.compiler.parser.soft + +import fastparse._ +import fastparse.NoWhitespace.noWhitespaceImplicit +import org.alephium.ralph.lsp.access.compiler.message.SourceIndexExtra._ +import org.alephium.ralph.lsp.access.compiler.parser.soft.ast.{SoftAST, Token} + +object NumberParser { + + def parseOrFail[Unknown: P]: P[SoftAST.Number] = + P { + Index ~ + CommentParser.parseOrFail.? ~ + number ~ + SpaceParser.parseOrFail.? ~ + TokenParser.parseOrFail(Token.AlphLowercase).? ~ + Index + } map { + case (from, documentation, digits, postDigitSpace, unit, to) => + SoftAST.Number( + index = range(from, to), + documentation = documentation, + number = digits, + space = postDigitSpace, + unit = unit + ) + } + + private def number[Unknown: P]: P[SoftAST.CodeString] = + P { + Index ~ + isNumber ~ + (!Token.AlphLowercase.lexeme ~ CharIn("[0-9a-zA-Z]._+\\-")).rep(1).! ~ + Index + } map { + case (from, number, to) => + SoftAST.CodeString( + index = range(from, to), + text = number + ) + } + + private def isNumber[Unknown: P]: P[Unit] = + P(&(CharIn("+\\-").? ~ CharIn("[0-9]"))) + +} diff --git a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/SoftAST.scala b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/SoftAST.scala index 724bb32a1..9a43f08b1 100644 --- a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/SoftAST.scala +++ b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/SoftAST.scala @@ -408,6 +408,14 @@ object SoftAST { postTupleSpace: Option[Space]) extends ExpressionAST + case class Number( + index: SourceIndex, + documentation: Option[Comments], + number: CodeString, + space: Option[Space], + unit: Option[TokenDocumented[Token.AlphLowercase.type]]) + extends ExpressionAST + sealed trait SpaceAST extends SoftAST case class Space( diff --git a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/Token.scala b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/Token.scala index 168b56f4d..ced264393 100644 --- a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/Token.scala +++ b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/Token.scala @@ -146,8 +146,8 @@ object Token { case object False extends PrimitiveBoolean("false") with Reserved sealed abstract class PrimitiveUnit(override val lexeme: String) extends Primitive(lexeme) - case object Alph_Small extends PrimitiveUnit("alph") with Reserved - case object Alph_Big extends PrimitiveUnit("ALPH") with Reserved + case object AlphLowercase extends PrimitiveUnit("alph") with Reserved + case object AlphUppercase extends PrimitiveUnit("ALPH") with Reserved sealed trait Term extends Token case class Name(lexeme: String) extends Term diff --git a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/NumberParserSpec.scala b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/NumberParserSpec.scala new file mode 100644 index 000000000..21a25d560 --- /dev/null +++ b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/NumberParserSpec.scala @@ -0,0 +1,216 @@ +package org.alephium.ralph.lsp.access.compiler.parser.soft + +import org.alephium.ralph.lsp.access.compiler.parser.soft.TestParser._ +import org.alephium.ralph.lsp.access.compiler.parser.soft.ast.SoftAST +import org.alephium.ralph.lsp.access.compiler.parser.soft.ast.TestSoftAST._ +import org.alephium.ralph.lsp.access.util.TestCodeUtil._ +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class NumberParserSpec extends AnyWordSpec with Matchers { + + def assertSimpleNumber(number: String) = + parseNumber(number) shouldBe + Number( + index = indexOf(s">>$number<<"), + text = number + ) + + "integer" in { + assertSimpleNumber("10") + assertSimpleNumber("-10") + assertSimpleNumber("+10") + } + + "typed" in { + assertSimpleNumber("10u") + assertSimpleNumber("10i") + assertSimpleNumber("-10u") + assertSimpleNumber("-10i") + assertSimpleNumber("+10u") + assertSimpleNumber("+10i") + } + + "scientific" in { + assertSimpleNumber("1e18") + assertSimpleNumber("-1e18") + assertSimpleNumber("+1e18") + assertSimpleNumber("5.12e18") + assertSimpleNumber("-5.12e18") + assertSimpleNumber("+5.12e18") + } + + "underscored" in { + assertSimpleNumber("1_000_000_000") + assertSimpleNumber("-1_000_000_000") + assertSimpleNumber("+1_000_000_000") + } + + "hex" in { + assertSimpleNumber("0x12") + assertSimpleNumber("-0x12") + assertSimpleNumber("+0x12") + } + + "unit" when { + "invalid" in { + assertSimpleNumber("1alp") + assertSimpleNumber("-1alp") + assertSimpleNumber("+1alp") + + assertSimpleNumber("1lph") + assertSimpleNumber("-1lph") + assertSimpleNumber("+1lph") + + assertSimpleNumber("1hpla") + assertSimpleNumber("-1hpla") + assertSimpleNumber("+1hpla") + } + + "valid" when { + "no spaces" when { + def testWithUnit(numberOnly: String) = + parseNumber(s"${numberOnly}alph") shouldBe + SoftAST.Number( + index = indexOf(s">>${numberOnly}alph<<"), + documentation = None, + number = SoftAST.CodeString( + index = indexOf(s">>$numberOnly<<alph"), + text = numberOnly + ), + space = None, + unit = Some(AlphLowercase(indexOf(s"$numberOnly>>alph<<"))) + ) + + "no sign" in { + testWithUnit("1") + testWithUnit("5.12e18") + testWithUnit("0x12") + } + + "positive" in { + testWithUnit("+1") + testWithUnit("+5.12e18") + testWithUnit("+0x12") + } + + "negative" in { + testWithUnit("-1") + testWithUnit("-5.12e18") + testWithUnit("-0x12") + } + } + + "with space" when { + def testWithUnit(numberOnly: String) = + parseNumber(s"$numberOnly alph") shouldBe + SoftAST.Number( + index = indexOf(s">>$numberOnly alph<<"), + documentation = None, + number = SoftAST.CodeString( + index = indexOf(s">>$numberOnly<< alph"), + text = numberOnly + ), + space = Some(SpaceOne(indexOf(s"$numberOnly>> <<alph"))), + unit = Some(AlphLowercase(indexOf(s"$numberOnly >>alph<<"))) + ) + + "no sign" in { + testWithUnit("1") + testWithUnit("5.12e18") + testWithUnit("0x12") + } + + "positive" in { + testWithUnit("+1") + testWithUnit("+5.12e18") + testWithUnit("+0x12") + } + + "negative" in { + testWithUnit("-1") + testWithUnit("-5.12e18") + testWithUnit("-0x12") + } + } + + "scientific number" when { + "without space" when { + "valid unit" in { + parseNumber("1e-18alph") shouldBe + SoftAST.Number( + index = indexOf(s">>1e-18alph<<"), + documentation = None, + number = SoftAST.CodeString( + index = indexOf(s">>1e-18<<alph"), + text = "1e-18" + ), + space = None, + unit = Some(AlphLowercase(indexOf("1e-18>>alph<<"))) + ) + } + + "invalid unit - 'alp' is typo" in { + parseNumber("1e-18alp") shouldBe + SoftAST.Number( + index = indexOf(s">>1e-18alp<<"), + documentation = None, + number = SoftAST.CodeString( + index = indexOf(s">>1e-18alp<<"), + text = "1e-18alp" + ), + space = None, + unit = None + ) + } + } + + "with space" when { + "valid unit" in { + parseNumber("1e-18 alph") shouldBe + SoftAST.Number( + index = indexOf(s">>1e-18 alph<<"), + documentation = None, + number = SoftAST.CodeString( + index = indexOf(s">>1e-18<< alph"), + text = "1e-18" + ), + space = Some(SpaceOne(indexOf("1e-18>> <<alph"))), + unit = Some(AlphLowercase(indexOf("1e-18 >>alph<<"))) + ) + } + + "invalid unit - 'alp' is typo" in { + val body = parseSoft("1e-18 alp") + body.parts should have size 2 + + val number = body.parts.head.part + val alp = body.parts.last.part + + // Note: alp is not a unit. So it's not parsed as part of the number. + number shouldBe + SoftAST.Number( + index = indexOf(s">>1e-18 << alp"), + documentation = None, + number = SoftAST.CodeString( + index = indexOf(s">>1e-18<< alp"), + text = "1e-18" + ), + space = Some(SpaceOne(indexOf("1e-18>> <<alp"))), + unit = None + ) + + // alp is stored as an identifier + alp shouldBe + Identifier( + index = indexOf(s"1e-18 >>alp<<"), + text = "alp" + ) + } + } + + } + } + } + +} diff --git a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TestParser.scala b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TestParser.scala index 2efad43d6..ef7cd3201 100644 --- a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TestParser.scala +++ b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/TestParser.scala @@ -66,6 +66,9 @@ object TestParser { def parseBoolean(code: String): SoftAST.TokenExpression[Token.PrimitiveBoolean] = runSoftParser(BooleanParser.parseOrFail(_))(code) + def parseNumber(code: String): SoftAST.Number = + runSoftParser(NumberParser.parseOrFail(_))(code) + def findAnnotation(identifier: String)(code: String): Option[SoftAST.Annotation] = findAnnotation( identifier = identifier, diff --git a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/TestSoftAST.scala b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/TestSoftAST.scala index d7df5cb21..84bd5f3e6 100644 --- a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/TestSoftAST.scala +++ b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/compiler/parser/soft/ast/TestSoftAST.scala @@ -90,6 +90,18 @@ object TestSoftAST { token = Token.False ) + def AlphLowercase(index: SourceIndex): SoftAST.TokenDocumented[Token.AlphLowercase.type] = + TokenDocumented( + index = index, + token = Token.AlphLowercase + ) + + def AlphUppercase(index: SourceIndex): SoftAST.TokenDocumented[Token.AlphUppercase.type] = + TokenDocumented( + index = index, + token = Token.AlphUppercase + ) + def Identifier( index: SourceIndex, text: String): SoftAST.Identifier = @@ -102,6 +114,20 @@ object TestSoftAST { ) ) + def Number( + index: SourceIndex, + text: String): SoftAST.Number = + SoftAST.Number( + index = index, + documentation = None, + number = SoftAST.CodeString( + index = index, + text = text + ), + space = None, + unit = None + ) + def Unresolved( index: SourceIndex, text: String): SoftAST.Unresolved =