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 =