From 90678043fd1aac8d5bd777eda6c33b066ae91210 Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Tue, 29 Oct 2019 17:30:35 +0100 Subject: [PATCH 1/6] Favour error over implicit conversions with generic number literals --- .../dotty/tools/dotc/core/Definitions.scala | 9 ++-- .../src/dotty/tools/dotc/core/StdNames.scala | 3 ++ .../src/dotty/tools/dotc/typer/Typer.scala | 27 ++++++------ library/src/scala/util/FromDigits.scala | 24 +++++++++++ tests/neg-macros/generic-num-lits.check | 42 +++++++++++++++++++ tests/neg-macros/generic-num-lits.scala | 16 +++++++ 6 files changed, 106 insertions(+), 15 deletions(-) create mode 100644 tests/neg-macros/generic-num-lits.check create mode 100644 tests/neg-macros/generic-num-lits.scala diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index df5f6194d8ed..8c9e02f655ce 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -689,9 +689,12 @@ 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 FromDigits_WithRadixClass: ClassSymbol = ctx.requiredClass("scala.util.FromDigits.WithRadix") + @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..166f43124e48 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -528,20 +528,23 @@ 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 { + 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(arg, _, _) => - val fromDigits = untpd.Select(untpd.TypedSplice(arg), nme.fromDigits).withSpan(tree.span) + val summoned = untpd.Apply(untpd.TypedSplice(ref(summoner)), untpd.TypedSplice(arg) :: Nil).setGivenApply() + var fromDigits: untpd.Tree = 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 - case _ => Nil - } + val otherArgs = + if arg.tpe.widen.classSymbol.isSubClass(defn.FromDigits_WithRadixClass) then + tree.kind match + case Whole(r) if r != 10 => Literal(Constant(r)) :: Nil + case _ => Nil + else + Nil var app: untpd.Tree = untpd.Apply(fromDigits, firstArg :: otherArgs) if (ctx.mode.is(Mode.Pattern)) app = untpd.Block(Nil, app) return typed(app, pt) diff --git a/library/src/scala/util/FromDigits.scala b/library/src/scala/util/FromDigits.scala index 605e4047cb00..eb36cedf2a35 100644 --- a/library/src/scala/util/FromDigits.scala +++ b/library/src/scala/util/FromDigits.scala @@ -47,6 +47,30 @@ object FromDigits { */ trait Floating[T] extends Decimal[T] + inline def fromDigits[T](given x: FromDigits[T]): x.type = x + + inline def fromRadixDigits[T](given x: FromDigits[T]): x.type = + ${summonDigitsImpl[x.type, FromDigits.WithRadix[T]]('x)} + + inline def fromDecimalDigits[T](given x: FromDigits[T]): x.type = + ${summonDigitsImpl[x.type, FromDigits.Decimal[T]]('x)} + + inline def fromFloatingDigits[T](given x: FromDigits[T]): x.type = + ${summonDigitsImpl[x.type, FromDigits.Floating[T]]('x)} + + private def summonDigitsImpl[T <: FromDigits[_], U <: FromDigits[_]](x: Expr[T])(given + qctx: QuoteContext, t: Type[T], u: Type[U]): Expr[T] = { + import qctx.tasty.{Type => _, _, given} + def makeError = s"""|FromDigits instance is incompatible with the expected numeric kind. + | Found: ${t.show}(${x.show}) + | Expected: ${u.show}""".stripMargin + if typeOf[T] <:< typeOf[U] then + x + else + qctx.error(makeError, x) + Expr.nullExpr.cast[T] + } + /** The base type for exceptions that can be thrown from * `fromDigits` conversions */ diff --git a/tests/neg-macros/generic-num-lits.check b/tests/neg-macros/generic-num-lits.check new file mode 100644 index 000000000000..127fda795b3e --- /dev/null +++ b/tests/neg-macros/generic-num-lits.check @@ -0,0 +1,42 @@ +-- Error: tests/neg-macros/generic-num-lits.scala:2:8 ------------------------------------------------------------------ +2 |val b = 0xcafebabe: BigDecimal // error + | ^ + | FromDigits instance is incompatible with the expected numeric kind. + | Found: scala.util.FromDigits.BigDecimalFromDigits.type(scala.util.FromDigits.BigDecimalFromDigits) + | Expected: scala.util.FromDigits.WithRadix[scala.math.BigDecimal] + | This location is in code that was inlined at generic-num-lits.scala:2 +-- Error: tests/neg-macros/generic-num-lits.scala:3:8 ------------------------------------------------------------------ +3 |val c = 1.3: BigInt // error + | ^ + | FromDigits instance is incompatible with the expected numeric kind. + | Found: scala.util.FromDigits.BigIntFromDigits.type(scala.util.FromDigits.BigIntFromDigits) + | Expected: scala.util.FromDigits.Decimal[scala.math.BigInt] + | This location is in code that was inlined at generic-num-lits.scala:3 +-- Error: tests/neg-macros/generic-num-lits.scala:4:8 ------------------------------------------------------------------ +4 |val d = 2e500: BigInt // error + | ^ + | FromDigits instance is incompatible with the expected numeric kind. + | Found: scala.util.FromDigits.BigIntFromDigits.type(scala.util.FromDigits.BigIntFromDigits) + | Expected: scala.util.FromDigits.Floating[scala.math.BigInt] + | This location is in code that was inlined at generic-num-lits.scala:4 +-- Error: tests/neg-macros/generic-num-lits.scala:7:7 ------------------------------------------------------------------ +7 | case 0xcafebabe: BigDecimal => // error + | ^ + | FromDigits instance is incompatible with the expected numeric kind. + | Found: scala.util.FromDigits.BigDecimalFromDigits.type(scala.util.FromDigits.BigDecimalFromDigits) + | Expected: scala.util.FromDigits.WithRadix[scala.math.BigDecimal] + | This location is in code that was inlined at generic-num-lits.scala:7 +-- Error: tests/neg-macros/generic-num-lits.scala:11:7 ----------------------------------------------------------------- +11 | case 1.3: BigInt => // error + | ^ + | FromDigits instance is incompatible with the expected numeric kind. + | Found: scala.util.FromDigits.BigIntFromDigits.type(scala.util.FromDigits.BigIntFromDigits) + | Expected: scala.util.FromDigits.Decimal[scala.math.BigInt] + | This location is in code that was inlined at generic-num-lits.scala:11 +-- Error: tests/neg-macros/generic-num-lits.scala:15:7 ----------------------------------------------------------------- +15 | case 2e500: BigInt => // error + | ^ + | FromDigits instance is incompatible with the expected numeric kind. + | Found: scala.util.FromDigits.BigIntFromDigits.type(scala.util.FromDigits.BigIntFromDigits) + | Expected: scala.util.FromDigits.Floating[scala.math.BigInt] + | This location is in code that was inlined at generic-num-lits.scala:15 diff --git a/tests/neg-macros/generic-num-lits.scala b/tests/neg-macros/generic-num-lits.scala new file mode 100644 index 000000000000..355873deb293 --- /dev/null +++ b/tests/neg-macros/generic-num-lits.scala @@ -0,0 +1,16 @@ +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 + () From 442c735cdf694a112ce0731dd514b046c42e9cef Mon Sep 17 00:00:00 2001 From: bishabosha Date: Wed, 30 Oct 2019 01:06:23 +0100 Subject: [PATCH 2/6] more friendly error messages for FromDigits summoners --- library/src/scala/util/FromDigits.scala | 27 ++++++++++++------------- tests/neg-macros/generic-num-lits.check | 12 +++++------ 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/library/src/scala/util/FromDigits.scala b/library/src/scala/util/FromDigits.scala index eb36cedf2a35..27785ac8aec2 100644 --- a/library/src/scala/util/FromDigits.scala +++ b/library/src/scala/util/FromDigits.scala @@ -50,26 +50,25 @@ object FromDigits { inline def fromDigits[T](given x: FromDigits[T]): x.type = x inline def fromRadixDigits[T](given x: FromDigits[T]): x.type = - ${summonDigitsImpl[x.type, FromDigits.WithRadix[T]]('x)} + ${summonDigitsImpl[x.type, FromDigits.WithRadix[T], T]('x, "whole numbers with radix other than 10")} inline def fromDecimalDigits[T](given x: FromDigits[T]): x.type = - ${summonDigitsImpl[x.type, FromDigits.Decimal[T]]('x)} + ${summonDigitsImpl[x.type, FromDigits.Decimal[T], T]('x, "numbers with a decimal point")} inline def fromFloatingDigits[T](given x: FromDigits[T]): x.type = - ${summonDigitsImpl[x.type, FromDigits.Floating[T]]('x)} - - private def summonDigitsImpl[T <: FromDigits[_], U <: FromDigits[_]](x: Expr[T])(given - qctx: QuoteContext, t: Type[T], u: Type[U]): Expr[T] = { - import qctx.tasty.{Type => _, _, given} - def makeError = s"""|FromDigits instance is incompatible with the expected numeric kind. - | Found: ${t.show}(${x.show}) - | Expected: ${u.show}""".stripMargin - if typeOf[T] <:< typeOf[U] then + ${summonDigitsImpl[x.type, FromDigits.Floating[T], T]('x, "floating-point numbers")} + + private def summonDigitsImpl[Inst <: FromDigits[T], Expected <: FromDigits[T], T](x: Expr[Inst], descriptor: String) + (given qctx: QuoteContext, instance: Type[Inst], t: Type[T], expected: Type[Expected]): Expr[Inst] = + import qctx.tasty.{_, given} + if typeOf[Inst] <:< typeOf[Expected] then x else - qctx.error(makeError, x) - Expr.nullExpr.cast[T] - } + val msg = s"""|Type ${t.show} does not have a FromDigits instance for $descriptor. + | Found: ${instance.show}(${x.show}) + | Expected: ${expected.show}""".stripMargin + qctx.error(msg, x) + Expr.nullExpr.cast[Inst] /** The base type for exceptions that can be thrown from * `fromDigits` conversions diff --git a/tests/neg-macros/generic-num-lits.check b/tests/neg-macros/generic-num-lits.check index 127fda795b3e..f7d726c45784 100644 --- a/tests/neg-macros/generic-num-lits.check +++ b/tests/neg-macros/generic-num-lits.check @@ -1,42 +1,42 @@ -- Error: tests/neg-macros/generic-num-lits.scala:2:8 ------------------------------------------------------------------ 2 |val b = 0xcafebabe: BigDecimal // error | ^ - | FromDigits instance is incompatible with the expected numeric kind. + | Type scala.math.BigDecimal does not have a FromDigits instance for whole numbers with radix other than 10. | Found: scala.util.FromDigits.BigDecimalFromDigits.type(scala.util.FromDigits.BigDecimalFromDigits) | Expected: scala.util.FromDigits.WithRadix[scala.math.BigDecimal] | This location is in code that was inlined at generic-num-lits.scala:2 -- Error: tests/neg-macros/generic-num-lits.scala:3:8 ------------------------------------------------------------------ 3 |val c = 1.3: BigInt // error | ^ - | FromDigits instance is incompatible with the expected numeric kind. + | Type scala.math.BigInt does not have a FromDigits instance for numbers with a decimal point. | Found: scala.util.FromDigits.BigIntFromDigits.type(scala.util.FromDigits.BigIntFromDigits) | Expected: scala.util.FromDigits.Decimal[scala.math.BigInt] | This location is in code that was inlined at generic-num-lits.scala:3 -- Error: tests/neg-macros/generic-num-lits.scala:4:8 ------------------------------------------------------------------ 4 |val d = 2e500: BigInt // error | ^ - | FromDigits instance is incompatible with the expected numeric kind. + | Type scala.math.BigInt does not have a FromDigits instance for floating-point numbers. | Found: scala.util.FromDigits.BigIntFromDigits.type(scala.util.FromDigits.BigIntFromDigits) | Expected: scala.util.FromDigits.Floating[scala.math.BigInt] | This location is in code that was inlined at generic-num-lits.scala:4 -- Error: tests/neg-macros/generic-num-lits.scala:7:7 ------------------------------------------------------------------ 7 | case 0xcafebabe: BigDecimal => // error | ^ - | FromDigits instance is incompatible with the expected numeric kind. + | Type scala.math.BigDecimal does not have a FromDigits instance for whole numbers with radix other than 10. | Found: scala.util.FromDigits.BigDecimalFromDigits.type(scala.util.FromDigits.BigDecimalFromDigits) | Expected: scala.util.FromDigits.WithRadix[scala.math.BigDecimal] | This location is in code that was inlined at generic-num-lits.scala:7 -- Error: tests/neg-macros/generic-num-lits.scala:11:7 ----------------------------------------------------------------- 11 | case 1.3: BigInt => // error | ^ - | FromDigits instance is incompatible with the expected numeric kind. + | Type scala.math.BigInt does not have a FromDigits instance for numbers with a decimal point. | Found: scala.util.FromDigits.BigIntFromDigits.type(scala.util.FromDigits.BigIntFromDigits) | Expected: scala.util.FromDigits.Decimal[scala.math.BigInt] | This location is in code that was inlined at generic-num-lits.scala:11 -- Error: tests/neg-macros/generic-num-lits.scala:15:7 ----------------------------------------------------------------- 15 | case 2e500: BigInt => // error | ^ - | FromDigits instance is incompatible with the expected numeric kind. + | Type scala.math.BigInt does not have a FromDigits instance for floating-point numbers. | Found: scala.util.FromDigits.BigIntFromDigits.type(scala.util.FromDigits.BigIntFromDigits) | Expected: scala.util.FromDigits.Floating[scala.math.BigInt] | This location is in code that was inlined at generic-num-lits.scala:15 From d57714dcb0283a6298dcd9bc95f59eae12a60a08 Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Thu, 31 Oct 2019 10:33:00 +0100 Subject: [PATCH 3/6] alternative version using implicitNotFound --- .../dotty/tools/dotc/core/Definitions.scala | 1 - .../src/dotty/tools/dotc/typer/Typer.scala | 17 ++++---- library/src/scala/util/FromDigits.scala | 29 ++++--------- tests/neg-macros/generic-num-lits.check | 42 ------------------- tests/neg/generic-num-lits.check | 36 ++++++++++++++++ .../generic-num-lits.scala | 12 ++++++ 6 files changed, 64 insertions(+), 73 deletions(-) delete mode 100644 tests/neg-macros/generic-num-lits.check create mode 100644 tests/neg/generic-num-lits.check rename tests/{neg-macros => neg}/generic-num-lits.scala (65%) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 8c9e02f655ce..a6a937391895 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -689,7 +689,6 @@ 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 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) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 166f43124e48..d4544b40117b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -534,17 +534,14 @@ class Typer extends Namer case Decimal => defn.FromDigits_fromDecimalDigits case Floating => defn.FromDigits_fromFloatingDigits inferImplicit(defn.FromDigitsClass.typeRef.appliedTo(target), EmptyTree, tree.span) match { - case SearchSuccess(arg, _, _) => - val summoned = untpd.Apply(untpd.TypedSplice(ref(summoner)), untpd.TypedSplice(arg) :: Nil).setGivenApply() - var fromDigits: untpd.Tree = untpd.Select(summoned, nme.fromDigits).withSpan(tree.span) + 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 = - if arg.tpe.widen.classSymbol.isSubClass(defn.FromDigits_WithRadixClass) then - tree.kind match - case Whole(r) if r != 10 => Literal(Constant(r)) :: Nil - case _ => Nil - else - Nil + val otherArgs = tree.kind match { + case Whole(r) if r != 10 => Literal(Constant(r)) :: Nil + case _ => Nil + } var app: untpd.Tree = untpd.Apply(fromDigits, firstArg :: otherArgs) if (ctx.mode.is(Mode.Pattern)) app = untpd.Block(Nil, app) return typed(app, pt) diff --git a/library/src/scala/util/FromDigits.scala b/library/src/scala/util/FromDigits.scala index 27785ac8aec2..dfaca1b8a5a0 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,36 +41,23 @@ 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] inline def fromDigits[T](given x: FromDigits[T]): x.type = x - inline def fromRadixDigits[T](given x: FromDigits[T]): x.type = - ${summonDigitsImpl[x.type, FromDigits.WithRadix[T], T]('x, "whole numbers with radix other than 10")} - - inline def fromDecimalDigits[T](given x: FromDigits[T]): x.type = - ${summonDigitsImpl[x.type, FromDigits.Decimal[T], T]('x, "numbers with a decimal point")} - - inline def fromFloatingDigits[T](given x: FromDigits[T]): x.type = - ${summonDigitsImpl[x.type, FromDigits.Floating[T], T]('x, "floating-point numbers")} - - private def summonDigitsImpl[Inst <: FromDigits[T], Expected <: FromDigits[T], T](x: Expr[Inst], descriptor: String) - (given qctx: QuoteContext, instance: Type[Inst], t: Type[T], expected: Type[Expected]): Expr[Inst] = - import qctx.tasty.{_, given} - if typeOf[Inst] <:< typeOf[Expected] then - x - else - val msg = s"""|Type ${t.show} does not have a FromDigits instance for $descriptor. - | Found: ${instance.show}(${x.show}) - | Expected: ${expected.show}""".stripMargin - qctx.error(msg, x) - Expr.nullExpr.cast[Inst] + inline def fromRadixDigits[T](given x: FromDigits.WithRadix[T]): x.type = x + + inline def fromDecimalDigits[T](given x: FromDigits.Decimal[T]): x.type = x + + 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-macros/generic-num-lits.check b/tests/neg-macros/generic-num-lits.check deleted file mode 100644 index f7d726c45784..000000000000 --- a/tests/neg-macros/generic-num-lits.check +++ /dev/null @@ -1,42 +0,0 @@ --- Error: tests/neg-macros/generic-num-lits.scala:2:8 ------------------------------------------------------------------ -2 |val b = 0xcafebabe: BigDecimal // error - | ^ - | Type scala.math.BigDecimal does not have a FromDigits instance for whole numbers with radix other than 10. - | Found: scala.util.FromDigits.BigDecimalFromDigits.type(scala.util.FromDigits.BigDecimalFromDigits) - | Expected: scala.util.FromDigits.WithRadix[scala.math.BigDecimal] - | This location is in code that was inlined at generic-num-lits.scala:2 --- Error: tests/neg-macros/generic-num-lits.scala:3:8 ------------------------------------------------------------------ -3 |val c = 1.3: BigInt // error - | ^ - | Type scala.math.BigInt does not have a FromDigits instance for numbers with a decimal point. - | Found: scala.util.FromDigits.BigIntFromDigits.type(scala.util.FromDigits.BigIntFromDigits) - | Expected: scala.util.FromDigits.Decimal[scala.math.BigInt] - | This location is in code that was inlined at generic-num-lits.scala:3 --- Error: tests/neg-macros/generic-num-lits.scala:4:8 ------------------------------------------------------------------ -4 |val d = 2e500: BigInt // error - | ^ - | Type scala.math.BigInt does not have a FromDigits instance for floating-point numbers. - | Found: scala.util.FromDigits.BigIntFromDigits.type(scala.util.FromDigits.BigIntFromDigits) - | Expected: scala.util.FromDigits.Floating[scala.math.BigInt] - | This location is in code that was inlined at generic-num-lits.scala:4 --- Error: tests/neg-macros/generic-num-lits.scala:7:7 ------------------------------------------------------------------ -7 | case 0xcafebabe: BigDecimal => // error - | ^ - | Type scala.math.BigDecimal does not have a FromDigits instance for whole numbers with radix other than 10. - | Found: scala.util.FromDigits.BigDecimalFromDigits.type(scala.util.FromDigits.BigDecimalFromDigits) - | Expected: scala.util.FromDigits.WithRadix[scala.math.BigDecimal] - | This location is in code that was inlined at generic-num-lits.scala:7 --- Error: tests/neg-macros/generic-num-lits.scala:11:7 ----------------------------------------------------------------- -11 | case 1.3: BigInt => // error - | ^ - | Type scala.math.BigInt does not have a FromDigits instance for numbers with a decimal point. - | Found: scala.util.FromDigits.BigIntFromDigits.type(scala.util.FromDigits.BigIntFromDigits) - | Expected: scala.util.FromDigits.Decimal[scala.math.BigInt] - | This location is in code that was inlined at generic-num-lits.scala:11 --- Error: tests/neg-macros/generic-num-lits.scala:15:7 ----------------------------------------------------------------- -15 | case 2e500: BigInt => // error - | ^ - | Type scala.math.BigInt does not have a FromDigits instance for floating-point numbers. - | Found: scala.util.FromDigits.BigIntFromDigits.type(scala.util.FromDigits.BigIntFromDigits) - | Expected: scala.util.FromDigits.Floating[scala.math.BigInt] - | This location is in code that was inlined at generic-num-lits.scala:15 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-macros/generic-num-lits.scala b/tests/neg/generic-num-lits.scala similarity index 65% rename from tests/neg-macros/generic-num-lits.scala rename to tests/neg/generic-num-lits.scala index 355873deb293..d1e7e139b6c3 100644 --- a/tests/neg-macros/generic-num-lits.scala +++ b/tests/neg/generic-num-lits.scala @@ -14,3 +14,15 @@ val f = (??? : Any) match 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 + () From 0a720d8d7009369c1dc7b10407cb1f1ad07b0ac6 Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Fri, 1 Nov 2019 13:38:15 +0100 Subject: [PATCH 4/6] Update numeric literals documentation --- .../changed-features/numeric-literals.md | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/docs/reference/changed-features/numeric-literals.md b/docs/docs/reference/changed-features/numeric-literals.md index 562db962bf7a..0f86d50946df 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 @@ -112,6 +112,36 @@ class NumberTooSmall (msg: String = "number too small") extends FromDigi class MalformedNumber(msg: String = "malformed number literal") extends FromDigitsException(msg) ``` +### Compiler Expansion + +The companion object `FromDigits` also defines four methods that may be used to provide a given instance of one of the subclasses of `FromDigits`: +```scala +inline def fromDigits[T](given x: FromDigits[T]): x.type = x + +inline def fromRadixDigits[T](given x: FromDigits.WithRadix[T]): x.type = x + +inline def fromDecimalDigits[T](given x: FromDigits.Decimal[T]): x.type = x + +inline def fromFloatingDigits[T](given x: FromDigits.Floating[T]): x.type = x +``` + +If a numeric literal has a known expected type `T` that is not one of the primitive numeric types, then the compiler will search for a given instance of `FromDigits[T]`. If one exists, then the compiler expands the literal to a call on the `fromDigits` method on the result obtained from calling one of the above four methods. + +As an example, the literal below has a nonsensical expected type `BigDecimal`, which can not be constructed with hex digits: +```scala +0xCAFEBABE: BigDecimal +``` +Upon the compiler finding a given instance for `FromDigits[BigDecimal]`, the hex literal above expands to the following: +```scala +FromDigits.fromRadixDigits[BigDecimal].fromDigits("0CAFEBABE", 16) +``` +The given clause of `fromRadixDigits` asserts that the prior found `FromDigits` instance is a subtype of `FromDigits.WithRadix[BigDecimal]`, or else following error is issued: +```scala +1 |0xCAFEBABE: 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 +204,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 From 181e4502b41b8e2ebbfb58c2e094da3f0b89a008 Mon Sep 17 00:00:00 2001 From: bishabosha Date: Sun, 3 Nov 2019 14:38:44 +0100 Subject: [PATCH 5/6] fully document usage of FromDigits summoners. --- .../changed-features/numeric-literals.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/docs/reference/changed-features/numeric-literals.md b/docs/docs/reference/changed-features/numeric-literals.md index 0f86d50946df..7f1db020574d 100644 --- a/docs/docs/reference/changed-features/numeric-literals.md +++ b/docs/docs/reference/changed-features/numeric-literals.md @@ -125,19 +125,26 @@ inline def fromDecimalDigits[T](given x: FromDigits.Decimal[T]): x.type = x inline def fromFloatingDigits[T](given x: FromDigits.Floating[T]): x.type = x ``` -If a numeric literal has a known expected type `T` that is not one of the primitive numeric types, then the compiler will search for a given instance of `FromDigits[T]`. If one exists, then the compiler expands the literal to a call on the `fromDigits` method on the result obtained from calling one of the above four methods. +If a numeric literal has a concrete expected type `T` that is not one of the primitive numeric types, the compiler will search for a given instance of `FromDigits[T]`. If one exists, then the compiler will replace the numeric literal with an application of its digits to the `fromDigits` method on the result of the application of `T` to one of the above four methods, resulting in the following: + +- `fromDigits[T].fromDigits(digits)` if the literal forms a whole number with base-10. +- `fromRadixDigits[T].fromDigits(digits, 16)` if the literal forms a whole number with base-16. +- `fromDecimalDigits[T].fromDigits(digits)` if the literal is base-10 with a single decimal point. +- `fromFloatingDigits[T].fromDigits(digits)` if the literal is base-10 with an exponent and optionally a single decimal point. + + As an example, the literal below has a nonsensical expected type `BigDecimal`, which can not be constructed with hex digits: ```scala -0xCAFEBABE: BigDecimal +0xABC_123: BigDecimal ``` -Upon the compiler finding a given instance for `FromDigits[BigDecimal]`, the hex literal above expands to the following: +Upon the compiler finding a given instance for `FromDigits[BigDecimal]`, the hex literal above expands to the following, after removing numeric separators: ```scala -FromDigits.fromRadixDigits[BigDecimal].fromDigits("0CAFEBABE", 16) +FromDigits.fromRadixDigits[BigDecimal].fromDigits("0ABC123", 16) ``` The given clause of `fromRadixDigits` asserts that the prior found `FromDigits` instance is a subtype of `FromDigits.WithRadix[BigDecimal]`, or else following error is issued: ```scala -1 |0xCAFEBABE: BigDecimal +1 |0xABC_123: BigDecimal |^ |Type BigDecimal does not have a FromDigits instance for whole numbers with radix other than 10. ``` From 4155cf2ccb721dc89cdf259fdd36491da98bda15 Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Tue, 5 Nov 2019 16:36:00 +0100 Subject: [PATCH 6/6] more informative documentation of numeric-literals --- .../changed-features/numeric-literals.md | 56 ++++++++++--------- library/src/scala/util/FromDigits.scala | 8 +++ 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/docs/docs/reference/changed-features/numeric-literals.md b/docs/docs/reference/changed-features/numeric-literals.md index 7f1db020574d..cd99f5c0854e 100644 --- a/docs/docs/reference/changed-features/numeric-literals.md +++ b/docs/docs/reference/changed-features/numeric-literals.md @@ -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,37 +132,19 @@ class NumberTooSmall (msg: String = "number too small") extends FromDigi class MalformedNumber(msg: String = "malformed number literal") extends FromDigitsException(msg) ``` -### Compiler Expansion - -The companion object `FromDigits` also defines four methods that may be used to provide a given instance of one of the subclasses of `FromDigits`: -```scala -inline def fromDigits[T](given x: FromDigits[T]): x.type = x - -inline def fromRadixDigits[T](given x: FromDigits.WithRadix[T]): x.type = x - -inline def fromDecimalDigits[T](given x: FromDigits.Decimal[T]): x.type = x - -inline def fromFloatingDigits[T](given x: FromDigits.Floating[T]): x.type = x -``` - -If a numeric literal has a concrete expected type `T` that is not one of the primitive numeric types, the compiler will search for a given instance of `FromDigits[T]`. If one exists, then the compiler will replace the numeric literal with an application of its digits to the `fromDigits` method on the result of the application of `T` to one of the above four methods, resulting in the following: - -- `fromDigits[T].fromDigits(digits)` if the literal forms a whole number with base-10. -- `fromRadixDigits[T].fromDigits(digits, 16)` if the literal forms a whole number with base-16. -- `fromDecimalDigits[T].fromDigits(digits)` if the literal is base-10 with a single decimal point. -- `fromFloatingDigits[T].fromDigits(digits)` if the literal is base-10 with an exponent and optionally a single decimal point. +### 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 literal below has a nonsensical expected type `BigDecimal`, which can not be constructed with hex digits: +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 ``` -Upon the compiler finding a given instance for `FromDigits[BigDecimal]`, the hex literal above expands to the following, after removing numeric separators: +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) ``` -The given clause of `fromRadixDigits` asserts that the prior found `FromDigits` instance is a subtype of `FromDigits.WithRadix[BigDecimal]`, or else following error is issued: +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 |^ diff --git a/library/src/scala/util/FromDigits.scala b/library/src/scala/util/FromDigits.scala index dfaca1b8a5a0..b675c732332b 100644 --- a/library/src/scala/util/FromDigits.scala +++ b/library/src/scala/util/FromDigits.scala @@ -51,12 +51,20 @@ object FromDigits { @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