diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index df5f6194d8ed..a6a937391895 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -689,9 +689,11 @@ class Definitions { @tu lazy val Stats_doRecord: Symbol = StatsModule.requiredMethod("doRecord") @tu lazy val FromDigitsClass: ClassSymbol = ctx.requiredClass("scala.util.FromDigits") - @tu lazy val FromDigits_WithRadixClass: ClassSymbol = ctx.requiredClass("scala.util.FromDigits.WithRadix") - @tu lazy val FromDigits_DecimalClass: ClassSymbol = ctx.requiredClass("scala.util.FromDigits.Decimal") - @tu lazy val FromDigits_FloatingClass: ClassSymbol = ctx.requiredClass("scala.util.FromDigits.Floating") + @tu lazy val FromDigitsModule: TermSymbol = ctx.requiredModule("scala.util.FromDigits") + @tu lazy val FromDigits_fromDigits: Symbol = FromDigitsModule.requiredMethod(nme.fromDigits) + @tu lazy val FromDigits_fromRadixDigits: Symbol = FromDigitsModule.requiredMethod(nme.fromRadixDigits) + @tu lazy val FromDigits_fromDecimalDigits: Symbol = FromDigitsModule.requiredMethod(nme.fromDecimalDigits) + @tu lazy val FromDigits_fromFloatingDigits: Symbol = FromDigitsModule.requiredMethod(nme.fromFloatingDigits) @tu lazy val XMLTopScopeModule: Symbol = ctx.requiredModule("scala.xml.TopScope") diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index b4983df55b0b..654a673e6bc6 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -444,6 +444,9 @@ object StdNames { val flatMap: N = "flatMap" val foreach: N = "foreach" val fromDigits: N = "fromDigits" + val fromRadixDigits: N = "fromRadixDigits" + val fromDecimalDigits: N = "fromDecimalDigits" + val fromFloatingDigits: N = "fromFloatingDigits" val fromProduct: N = "fromProduct" val genericArrayOps: N = "genericArrayOps" val genericClass: N = "genericClass" diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 505da8883248..d4544b40117b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -528,15 +528,15 @@ class Typer extends Namer return lit(doubleFromDigits(digits)) else if (target.isValueType && isFullyDefined(target, ForceDegree.none)) { // If expected type is defined with a FromDigits instance, use that one - val fromDigitsCls = tree.kind match { - case Whole(10) => defn.FromDigitsClass - case Whole(_) => defn.FromDigits_WithRadixClass - case Decimal => defn.FromDigits_DecimalClass - case Floating => defn.FromDigits_FloatingClass - } - inferImplicit(fromDigitsCls.typeRef.appliedTo(target), EmptyTree, tree.span) match { - case SearchSuccess(arg, _, _) => - val fromDigits = untpd.Select(untpd.TypedSplice(arg), nme.fromDigits).withSpan(tree.span) + val summoner = tree.kind match + case Whole(10) => defn.FromDigits_fromDigits + case Whole(_) => defn.FromDigits_fromRadixDigits + case Decimal => defn.FromDigits_fromDecimalDigits + case Floating => defn.FromDigits_fromFloatingDigits + inferImplicit(defn.FromDigitsClass.typeRef.appliedTo(target), EmptyTree, tree.span) match { + case _: SearchSuccess => + val summoned = untpd.TypedSplice(ref(summoner).appliedToType(target)) + val fromDigits = untpd.Select(summoned, nme.fromDigits).withSpan(tree.span) val firstArg = Literal(Constant(digits)) val otherArgs = tree.kind match { case Whole(r) if r != 10 => Literal(Constant(r)) :: Nil diff --git a/docs/docs/reference/changed-features/numeric-literals.md b/docs/docs/reference/changed-features/numeric-literals.md index 562db962bf7a..cd99f5c0854e 100644 --- a/docs/docs/reference/changed-features/numeric-literals.md +++ b/docs/docs/reference/changed-features/numeric-literals.md @@ -45,7 +45,7 @@ val y: BigInt = 0x123_abc_789_def_345_678_901 val z: BigDecimal = 111222333444.55 ``` are legal by rule (2), since both `BigInt` and `BigDecimal` have `FromDigits` instances -(which implement the `FromDigits` subclasses `FromDigits.WithRadix` and `FromDigits.Decimal`, respectively). +(which implement the `FromDigits` subclasses `FromDigits.WithRadix` and `FromDigits.Floating`, respectively). On the other hand, ```scala val x = -10_000_000_000 @@ -70,7 +70,7 @@ the string is passed to `fromDigits`. The companion object `FromDigits` also defines subclasses of `FromDigits` for whole numbers with a given radix, for numbers with a decimal point, and for -numbers that can have both a decimal point and an exponent: +numbers that can have both a decimal point and an exponent, along with methods to summon a given instance of each subclass: ```scala object FromDigits { @@ -92,12 +92,32 @@ object FromDigits { * exponent `('e' | 'E')['+' | '-']digit digit*`. */ trait Floating[T] extends Decimal[T] + + /** Does `T` have a given `FromDigits[T]` instance? + */ + inline def fromDigits[T](given x: FromDigits[T]): x.type = x + + /** Does `T` have a given `FromDigits.WithRadix[T]` instance? + */ + inline def fromRadixDigits[T](given x: FromDigits.WithRadix[T]): x.type = x + + /** Does `T` have a given `FromDigits.Decimal[T]` instance? + */ + inline def fromDecimalDigits[T](given x: FromDigits.Decimal[T]): x.type = x + + /** Does `T` have a given `FromDigits.Floating[T]` instance? + */ + inline def fromFloatingDigits[T](given x: FromDigits.Floating[T]): x.type = x ... } ``` -A user-defined number type can implement one of those, which signals to the compiler -that hexadecimal numbers, decimal points, or exponents are also accepted in literals -for this type. +A user-defined number type, that provides a given instance of one of the above subclasses, signals to the compiler +that hexadecimal numbers, decimal points, or exponents are also accepted in literals for that type. A numeric literal with a concrete type `T` and digits `digits` is replaced as follows, when a given instance for `FromDigits[T]` is available: + +- `fromDigits[T].fromDigits(digits)` if the digits form a decimal whole number. +- `fromRadixDigits[T].fromDigits(digits, 16)` if the digits form a hexadecimal whole number. +- `fromDecimalDigits[T].fromDigits(digits)` if the digits are decimal with a single decimal point `"."`. +- `fromFloatingDigits[T].fromDigits(digits)` if the digits are decimal with an exponent and optionally a single decimal point `"."`. ### Error Handling @@ -112,6 +132,25 @@ class NumberTooSmall (msg: String = "number too small") extends FromDigi class MalformedNumber(msg: String = "malformed number literal") extends FromDigitsException(msg) ``` +### Number Format Safety + +A benefit of implementing one of the specialised subclasses of `FromDigits` is that the compiler preserves invariants about the format of digits passed to the `fromDigits` method. + +As an example, the constructor for `BigDecimal` can not accept digits in hexadecimal format, so it does not make sense to allow hexadecimal number literals with expected type `BigDecimal`. Below is such an expression: +```scala +0xABC_123: BigDecimal +``` +We can protect against this poorly formed expression by restricting the domain of acceptable number literals. The `FromDigits` companion object provides a given instance of `FromDigits.Floating[BigDecimal]`, which does not accept non-decimal literals. As described above, because the compiler can see a given instance for `FromDigits[BigDecimal]`, the hex literal is replaced with the following, after removing numeric separators: +```scala +FromDigits.fromRadixDigits[BigDecimal].fromDigits("0ABC123", 16) +``` +Because the given instance for `FromDigits[BigDecimal]` in the `FromDigits` companion object is not a subtype of `FromDigits.WithRadix[BigDecimal]`, as expected by the given clause of `fromRadixDigits`, the following error is issued: +```scala +1 |0xABC_123: BigDecimal + |^ + |Type BigDecimal does not have a FromDigits instance for whole numbers with radix other than 10. +``` + ### Example As a fully worked out example, here is an implementation of a new numeric class, `BigFloat`, that accepts numeric literals. `BigFloat` is defined in terms of a `BigInt` mantissa and an `Int` exponent: @@ -174,7 +213,7 @@ With the setup of the previous section, a literal like ``` would be expanded by the compiler to ```scala -BigFloat.FromDigits.fromDigits("1e100000000000") +FromDigits.fromFloatingDigits[BigFloat].fromDigits("1e100000000000") ``` Evaluating this expression throws a `NumberTooLarge` exception at run time. We would like it to produce a compile-time error instead. We can achieve this by tweaking the `BigFloat` class diff --git a/library/src/scala/util/FromDigits.scala b/library/src/scala/util/FromDigits.scala index 605e4047cb00..b675c732332b 100644 --- a/library/src/scala/util/FromDigits.scala +++ b/library/src/scala/util/FromDigits.scala @@ -4,6 +4,7 @@ import quoted._ import quoted.matching._ import internal.Chars.digit2int import annotation.internal.sharable +import annotation.implicitNotFound /** A typeclass for types that admit numeric literals. */ @@ -27,6 +28,7 @@ object FromDigits { /** A subclass of `FromDigits` that also allows to convert whole number literals * with a radix other than 10 */ + @implicitNotFound("Type ${T} does not have a FromDigits instance for whole numbers with radix other than 10.") trait WithRadix[T] extends FromDigits[T] { def fromDigits(digits: String): T = fromDigits(digits, 10) @@ -39,14 +41,32 @@ object FromDigits { /** A subclass of `FromDigits` that also allows to convert number * literals containing a decimal point ".". */ + @implicitNotFound("Type ${T} does not have a FromDigits instance for numbers with a decimal point.") trait Decimal[T] extends FromDigits[T] /** A subclass of `FromDigits`that allows also to convert number * literals containing a decimal point "." or an * exponent `('e' | 'E')['+' | '-']digit digit*`. */ + @implicitNotFound("Type ${T} does not have a FromDigits instance for floating-point numbers.") trait Floating[T] extends Decimal[T] + /** Does `T` have a given `FromDigits[T]` instance? + */ + inline def fromDigits[T](given x: FromDigits[T]): x.type = x + + /** Does `T` have a given `FromDigits.WithRadix[T]` instance? + */ + inline def fromRadixDigits[T](given x: FromDigits.WithRadix[T]): x.type = x + + /** Does `T` have a given `FromDigits.Decimal[T]` instance? + */ + inline def fromDecimalDigits[T](given x: FromDigits.Decimal[T]): x.type = x + + /** Does `T` have a given `FromDigits.Floating[T]` instance? + */ + inline def fromFloatingDigits[T](given x: FromDigits.Floating[T]): x.type = x + /** The base type for exceptions that can be thrown from * `fromDigits` conversions */ diff --git a/tests/neg/generic-num-lits.check b/tests/neg/generic-num-lits.check new file mode 100644 index 000000000000..fdcf2d64c5d4 --- /dev/null +++ b/tests/neg/generic-num-lits.check @@ -0,0 +1,36 @@ +-- Error: tests/neg/generic-num-lits.scala:2:8 ------------------------------------------------------------------------- +2 |val b = 0xcafebabe: BigDecimal // error + | ^ + | Type BigDecimal does not have a FromDigits instance for whole numbers with radix other than 10. +-- Error: tests/neg/generic-num-lits.scala:3:8 ------------------------------------------------------------------------- +3 |val c = 1.3: BigInt // error + | ^ + | Type BigInt does not have a FromDigits instance for numbers with a decimal point. +-- Error: tests/neg/generic-num-lits.scala:4:8 ------------------------------------------------------------------------- +4 |val d = 2e500: BigInt // error + | ^ + | Type BigInt does not have a FromDigits instance for floating-point numbers. +-- Error: tests/neg/generic-num-lits.scala:7:7 ------------------------------------------------------------------------- +7 | case 0xcafebabe: BigDecimal => // error + | ^ + | Type BigDecimal does not have a FromDigits instance for whole numbers with radix other than 10. +-- Error: tests/neg/generic-num-lits.scala:11:7 ------------------------------------------------------------------------ +11 | case 1.3: BigInt => // error + | ^ + | Type BigInt does not have a FromDigits instance for numbers with a decimal point. +-- Error: tests/neg/generic-num-lits.scala:15:7 ------------------------------------------------------------------------ +15 | case 2e500: BigInt => // error + | ^ + | Type BigInt does not have a FromDigits instance for floating-point numbers. +-- Error: tests/neg/generic-num-lits.scala:19:7 ------------------------------------------------------------------------ +19 | case 0xa => // error + | ^ + | Type BigDecimal does not have a FromDigits instance for whole numbers with radix other than 10. +-- Error: tests/neg/generic-num-lits.scala:23:7 ------------------------------------------------------------------------ +23 | case 1.2 => // error + | ^ + | Type BigInt does not have a FromDigits instance for numbers with a decimal point. +-- Error: tests/neg/generic-num-lits.scala:27:7 ------------------------------------------------------------------------ +27 | case 5e10 => // error + | ^ + | Type BigInt does not have a FromDigits instance for floating-point numbers. diff --git a/tests/neg/generic-num-lits.scala b/tests/neg/generic-num-lits.scala new file mode 100644 index 000000000000..d1e7e139b6c3 --- /dev/null +++ b/tests/neg/generic-num-lits.scala @@ -0,0 +1,28 @@ +val a = 0.25: BigDecimal +val b = 0xcafebabe: BigDecimal // error +val c = 1.3: BigInt // error +val d = 2e500: BigInt // error + +val e = (??? : Any) match + case 0xcafebabe: BigDecimal => // error + () + +val f = (??? : Any) match + case 1.3: BigInt => // error + () + +val g = (??? : Any) match + case 2e500: BigInt => // error + () + +val h = (1.3: BigDecimal) match + case 0xa => // error + () + +val i = (1: BigInt) match + case 1.2 => // error + () + +val j = (1: BigInt) match + case 5e10 => // error + ()