diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 743b0f14cf88..f8a9093d0739 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -109,6 +109,13 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case class ContextBounds(bounds: TypeBoundsTree, cxBounds: List[Tree])(implicit @constructorOnly src: SourceFile) extends TypTree case class PatDef(mods: Modifiers, pats: List[Tree], tpt: Tree, rhs: Tree)(implicit @constructorOnly src: SourceFile) extends DefTree case class Export(impliedOnly: Boolean, expr: Tree, selectors: List[Tree])(implicit @constructorOnly src: SourceFile) extends Tree + case class Number(digits: String, kind: NumberKind)(implicit @constructorOnly src: SourceFile) extends TermTree + + enum NumberKind { + case Whole(radix: Int) + case Decimal + case Floating + } /** Short-lived usage in typer, does not need copy/transform/fold infrastructure */ case class DependentTypeTree(tp: List[Symbol] => Type)(implicit @constructorOnly src: SourceFile) extends Tree @@ -558,6 +565,10 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case tree: Export if (impliedOnly == tree.impliedOnly) && (expr eq tree.expr) && (selectors eq tree.selectors) => tree case _ => finalize(tree, untpd.Export(impliedOnly, expr, selectors)(tree.source)) } + def Number(tree: Tree)(digits: String, kind: NumberKind)(implicit ctx: Context): Tree = tree match { + case tree: Number if (digits == tree.digits) && (kind == tree.kind) => tree + case _ => finalize(tree, untpd.Number(digits, kind)) + } def TypedSplice(tree: Tree)(splice: tpd.Tree)(implicit ctx: Context): ProxyTree = tree match { case tree: TypedSplice if splice `eq` tree.splice => tree case _ => finalize(tree, untpd.TypedSplice(splice)(ctx)) @@ -612,7 +623,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { cpy.PatDef(tree)(mods, transform(pats), transform(tpt), transform(rhs)) case Export(impliedOnly, expr, selectors) => cpy.Export(tree)(impliedOnly, transform(expr), selectors) - case TypedSplice(_) => + case Number(_, _) | TypedSplice(_) => tree case _ => super.transformMoreCases(tree) @@ -667,6 +678,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { this(this(this(x, pats), tpt), rhs) case Export(_, expr, _) => this(x, expr) + case Number(_, _) => + x case TypedSplice(splice) => this(x, splice) case _ => diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index f1982974ea36..80254f917f6c 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -669,6 +669,11 @@ class Definitions { @tu lazy val StatsModule: Symbol = ctx.requiredModule("dotty.tools.dotc.util.Stats") @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 XMLTopScopeModule: Symbol = ctx.requiredModule("scala.xml.TopScope") @tu lazy val CommandLineParserModule: Symbol = ctx.requiredModule("scala.util.CommandLineParser") diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 03da138f04a5..0ba484b3e248 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -442,6 +442,7 @@ object StdNames { val flagsFromBits : N = "flagsFromBits" val flatMap: N = "flatMap" val foreach: N = "foreach" + val fromDigits: N = "fromDigits" val fromProduct: N = "fromProduct" val genericArrayOps: N = "genericArrayOps" val genericClass: N = "genericClass" diff --git a/compiler/src/dotty/tools/dotc/parsing/JavaScanners.scala b/compiler/src/dotty/tools/dotc/parsing/JavaScanners.scala index 7a8e17cbadf5..0e437d9d0a49 100644 --- a/compiler/src/dotty/tools/dotc/parsing/JavaScanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/JavaScanners.scala @@ -519,20 +519,13 @@ object JavaScanners { // Errors ----------------------------------------------------------------- override def toString(): String = token match { - case IDENTIFIER => - "id(" + name + ")" - case CHARLIT => - "char(" + intVal + ")" - case INTLIT => - "int(" + intVal + ")" - case LONGLIT => - "long(" + intVal + ")" - case FLOATLIT => - "float(" + floatVal + ")" - case DOUBLELIT => - "double(" + doubleVal + ")" - case STRINGLIT => - "string(" + name + ")" + case IDENTIFIER => s"id($name)" + case CHARLIT => s"char($strVal)" + case INTLIT => s"int($strVal, $base)" + case LONGLIT => s"long($strVal, $base)" + case FLOATLIT => s"float($strVal)" + case DOUBLELIT => s"double($strVal)" + case STRINGLIT => s"string($strVal)" case SEMI => ";" case COMMA => diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index d3077292c45c..bf6ce69a15d1 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1016,24 +1016,37 @@ object Parsers { * @param negOffset The offset of a preceding `-' sign, if any. * If the literal is not negated, negOffset = in.offset. */ - def literal(negOffset: Int = in.offset, inPattern: Boolean = false, inStringInterpolation: Boolean = false): Tree = { - - def literalOf(token: Token): Literal = { + def literal(negOffset: Int = in.offset, inPattern: Boolean = false, inType: Boolean = false, inStringInterpolation: Boolean = false): Tree = { + def literalOf(token: Token): Tree = { val isNegated = negOffset < in.offset - val value = token match { - case CHARLIT => in.charVal - case INTLIT => in.intVal(isNegated).toInt - case LONGLIT => in.intVal(isNegated) - case FLOATLIT => in.floatVal(isNegated).toFloat - case DOUBLELIT => in.doubleVal(isNegated) - case STRINGLIT | STRINGPART => in.strVal - case TRUE => true - case FALSE => false - case NULL => null - case _ => - syntaxErrorOrIncomplete(IllegalLiteral()) - null - } + def digits0 = in.removeNumberSeparators(in.strVal) + def digits = if (isNegated) "-" + digits0 else digits0 + if (!inType) + token match { + case INTLIT => return Number(digits, NumberKind.Whole(in.base)) + case DECILIT => return Number(digits, NumberKind.Decimal) + case EXPOLIT => return Number(digits, NumberKind.Floating) + case _ => + } + import scala.util.FromDigits._ + val value = + try token match { + case INTLIT => intFromDigits(digits, in.base) + case LONGLIT => longFromDigits(digits, in.base) + case FLOATLIT => floatFromDigits(digits) + case DOUBLELIT | DECILIT | EXPOLIT => doubleFromDigits(digits) + case CHARLIT => in.strVal.head + case STRINGLIT | STRINGPART => in.strVal + case TRUE => true + case FALSE => false + case NULL => null + case _ => + syntaxErrorOrIncomplete(IllegalLiteral()) + null + } + catch { + case ex: FromDigitsException => syntaxErrorOrIncomplete(ex.getMessage) + } Literal(Constant(value)) } @@ -1163,6 +1176,7 @@ object Parsers { } /* ------------- TYPES ------------------------------------------------------ */ + /** Same as [[typ]], but if this results in a wildcard it emits a syntax error and * returns a tree for type `Any` instead. */ @@ -1378,11 +1392,11 @@ object Parsers { } else if (in.token == LBRACE) atSpan(in.offset) { RefinedTypeTree(EmptyTree, refinement()) } - else if (isSimpleLiteral) { SingletonTypeTree(literal()) } + else if (isSimpleLiteral) { SingletonTypeTree(literal(inType = true)) } else if (isIdent(nme.raw.MINUS) && in.lookaheadIn(numericLitTokens)) { val start = in.offset in.nextToken() - SingletonTypeTree(literal(negOffset = start)) + SingletonTypeTree(literal(negOffset = start, inType = true)) } else if (in.token == USCORE) { if (ctx.settings.strict.value) { @@ -2329,12 +2343,11 @@ object Parsers { if (isIdent(nme.raw.BAR)) { in.nextToken(); pattern1() :: patternAlts() } else Nil - /** Pattern1 ::= PatVar Ascription - * | Pattern2 + /** Pattern1 ::= Pattern2 [Ascription] */ def pattern1(): Tree = { val p = pattern2() - if (isVarPattern(p) && in.token == COLON) { + if (in.token == COLON) { in.nextToken() ascription(p, Location.InPattern) } diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index c673360ba421..2dbcf42ab278 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -123,99 +123,10 @@ object Scanners { def setStrVal(): Unit = strVal = flushBuf(litBuf) - /** Convert current strVal to char value - */ - def charVal: Char = if (strVal.length > 0) strVal.charAt(0) else 0 - - /** Convert current strVal, base to long value - * This is tricky because of max negative value. - */ - def intVal(negated: Boolean): Long = - if (token == CHARLIT && !negated) - charVal - else { - var value: Long = 0 - val divider = if (base == 10) 1 else 2 - val limit: Long = - if (token == LONGLIT) Long.MaxValue else Int.MaxValue - var i = 0 - val len = strVal.length - while (i < len) { - val c = strVal charAt i - if (! isNumberSeparator(c)) { - val d = digit2int(c, base) - if (d < 0) { - error(s"malformed integer number") - return 0 - } - if (value < 0 || - limit / (base / divider) < value || - limit - (d / divider) < value * (base / divider) && - !(negated && limit == value * base - 1 + d)) { - error("integer number too large") - return 0 - } - value = value * base + d - } - i += 1 - } - if (negated) -value else value - } - - def intVal: Long = intVal(false) - - private val zeroFloat = raw"[0.]+(?:[eE][+-]?[0-9]+)?[fFdD]?".r - - /** Convert current strVal, base to double value - */ - def floatVal(negated: Boolean): Float = { - assert(token == FLOATLIT) - val text = removeNumberSeparators(strVal) - try { - val value: Float = java.lang.Float.valueOf(text).floatValue() - if (value > Float.MaxValue) - errorButContinue("floating point number too large") - - if (value == 0.0f && !zeroFloat.pattern.matcher(text).matches) - errorButContinue("floating point number too small") - if (negated) -value else value - } - catch { - case _: NumberFormatException => - error("malformed floating point number") - 0.0f - } - } - - def floatVal: Float = floatVal(false) - - /** Convert current strVal, base to double value - */ - def doubleVal(negated: Boolean): Double = { - assert(token == DOUBLELIT) - val text = removeNumberSeparators(strVal) - try { - val value: Double = java.lang.Double.valueOf(text).doubleValue() - if (value > Double.MaxValue) - errorButContinue("double precision floating point number too large") - - if (value == 0.0d && !zeroFloat.pattern.matcher(text).matches) - errorButContinue("double precision floating point number too small") - if (negated) -value else value - } - catch { - case _: NumberFormatException => - error("malformed floating point number") - 0.0 - } - } - - def doubleVal: Double = doubleVal(false) - @inline def isNumberSeparator(c: Char): Boolean = c == '_' @inline def removeNumberSeparators(s: String): String = - if (s.indexOf('_') > 0) s.replaceAllLiterally("_", "") /*.replaceAll("'","")*/ else s + if (s.indexOf('_') > 0) s.replaceAllLiterally("_", "") else s // disallow trailing numeric separator char, but continue lexing def checkNoTrailingSeparator(): Unit = @@ -1228,7 +1139,7 @@ object Scanners { * if one is present. */ protected def getFraction(): Unit = { - token = DOUBLELIT + token = DECILIT while ('0' <= ch && ch <= '9' || isNumberSeparator(ch)) { putChar(ch) nextChar() @@ -1252,7 +1163,7 @@ object Scanners { } checkNoTrailingSeparator() } - token = DOUBLELIT + token = EXPOLIT } if (ch == 'd' || ch == 'D') { putChar(ch) @@ -1329,11 +1240,11 @@ object Scanners { def show: String = token match { case IDENTIFIER | BACKQUOTED_IDENT => s"id($name)" - case CHARLIT => s"char($intVal)" - case INTLIT => s"int($intVal)" - case LONGLIT => s"long($intVal)" - case FLOATLIT => s"float($floatVal)" - case DOUBLELIT => s"double($doubleVal)" + case CHARLIT => s"char($strVal)" + case INTLIT => s"int($strVal, $base)" + case LONGLIT => s"long($strVal, $base)" + case FLOATLIT => s"float($strVal)" + case DOUBLELIT => s"double($strVal)" case STRINGLIT => s"string($strVal)" case STRINGPART => s"stringpart($strVal)" case INTERPOLATIONID => s"interpolationid($name)" diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index 96acb0557791..cbe9ae05c4b5 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -37,17 +37,19 @@ abstract class TokensCommon { /** literals */ final val CHARLIT = 3; enter(CHARLIT, "character literal") final val INTLIT = 4; enter(INTLIT, "integer literal") - final val LONGLIT = 5; enter(LONGLIT, "long literal") - final val FLOATLIT = 6; enter(FLOATLIT, "float literal") - final val DOUBLELIT = 7; enter(DOUBLELIT, "double literal") - final val STRINGLIT = 8; enter(STRINGLIT, "string literal") - final val STRINGPART = 9; enter(STRINGPART, "string literal", "string literal part") - //final val INTERPOLATIONID = 10; enter(INTERPOLATIONID, "string interpolator") - //final val QUOTEID = 11; enter(QUOTEID, "quoted identifier") // TODO: deprecate + final val DECILIT = 5; enter(DECILIT, "number literal") // with decimal point + final val EXPOLIT = 6; enter(EXPOLIT, "number literal with exponent") + final val LONGLIT = 7; enter(LONGLIT, "long literal") + final val FLOATLIT = 8; enter(FLOATLIT, "float literal") + final val DOUBLELIT = 9; enter(DOUBLELIT, "double literal") + final val STRINGLIT = 10; enter(STRINGLIT, "string literal") + final val STRINGPART = 11; enter(STRINGPART, "string literal", "string literal part") + //final val INTERPOLATIONID = 12; enter(INTERPOLATIONID, "string interpolator") + //final val QUOTEID = 13; enter(QUOTEID, "quoted identifier") // TODO: deprecate /** identifiers */ - final val IDENTIFIER = 12; enter(IDENTIFIER, "identifier") - //final val BACKQUOTED_IDENT = 13; enter(BACKQUOTED_IDENT, "identifier", "backquoted ident") + final val IDENTIFIER = 14; enter(IDENTIFIER, "identifier") + //final val BACKQUOTED_IDENT = 15; enter(BACKQUOTED_IDENT, "identifier", "backquoted ident") /** alphabetic keywords */ final val IF = 20; enter(IF, "if") @@ -150,10 +152,10 @@ object Tokens extends TokensCommon { final val minToken = EMPTY final def maxToken: Int = XMLSTART - final val INTERPOLATIONID = 10; enter(INTERPOLATIONID, "string interpolator") - final val QUOTEID = 11; enter(QUOTEID, "quoted identifier") // TODO: deprecate + final val INTERPOLATIONID = 12; enter(INTERPOLATIONID, "string interpolator") + final val QUOTEID = 13; enter(QUOTEID, "quoted identifier") // TODO: deprecate - final val BACKQUOTED_IDENT = 13; enter(BACKQUOTED_IDENT, "identifier", "backquoted ident") + final val BACKQUOTED_IDENT = 15; enter(BACKQUOTED_IDENT, "identifier", "backquoted ident") final val identifierTokens: TokenSet = BitSet(IDENTIFIER, BACKQUOTED_IDENT) @@ -260,7 +262,7 @@ object Tokens extends TokensCommon { final val stopScanTokens: BitSet = mustStartStatTokens | BitSet(IF, ELSE, WHILE, DO, FOR, YIELD, NEW, TRY, CATCH, FINALLY, THROW, RETURN, MATCH, SEMI, EOF) - final val numericLitTokens: TokenSet = BitSet(INTLIT, LONGLIT, FLOATLIT, DOUBLELIT) + final val numericLitTokens: TokenSet = BitSet(INTLIT, DECILIT, EXPOLIT, LONGLIT, FLOATLIT, DOUBLELIT) final val statCtdTokens: BitSet = BitSet(THEN, ELSE, DO, CATCH, FINALLY, YIELD, MATCH) diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 9e4ab644b43d..24d8e5a6793a 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -604,6 +604,8 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { changePrec(GlobalPrec) { keywordStr("try ") ~ toText(expr) ~ " " ~ keywordStr("catch") ~ " {" ~ toText(handler) ~ "}" ~ optText(finalizer)(keywordStr(" finally ") ~ _) } + case Number(digits, kind) => + digits case Quote(tree) => if (tree.isType) keywordStr("'[") ~ toTextGlobal(dropBlock(tree)) ~ keywordStr("]") else keywordStr("'{") ~ toTextGlobal(dropBlock(tree)) ~ keywordStr("}") diff --git a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala index e39161435344..5763176b0923 100644 --- a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala +++ b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala @@ -591,8 +591,8 @@ private class ExtractAPICollector(implicit val ctx: Context) extends ThunkHolder def apiAnnotations(s: Symbol): List[api.Annotation] = { val annots = new mutable.ListBuffer[api.Annotation] - - if (Inliner.hasBodyToInline(s)) { + val inlineBody = Inliner.bodyToInline(s) + if (!inlineBody.isEmpty) { // FIXME: If the body of an inlineable method changes, all the reverse // dependencies of this method need to be recompiled. sbt has no way // of tracking method bodies, so as a hack we include the pretty-printed @@ -600,7 +600,7 @@ private class ExtractAPICollector(implicit val ctx: Context) extends ThunkHolder // To do this properly we would need a way to hash trees and types in // dotty itself. val printTypesCtx = ctx.fresh.setSetting(ctx.settings.XprintTypes, true) - annots += marker(Inliner.bodyToInline(s).show(printTypesCtx)) + annots += marker(inlineBody.show(printTypesCtx)) } // In the Scala2 ExtractAPI phase we only extract annotations that extend diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index f0cc631f1dd6..3ebc68c77833 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -339,17 +339,19 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic { } else Prod(erase(pat.tpe.stripAnnots), erase(fun.tpe), fun.symbol, pats.map(project), isIrrefutableUnapply(fun)) - case Typed(pat: UnApply, _) => - project(pat) - case Typed(expr, tpt) => + case Typed(expr @ Ident(nme.WILDCARD), tpt) => Typ(erase(expr.tpe.stripAnnots), true) + case Typed(pat, _) => + project(pat) case This(_) => Typ(pat.tpe.stripAnnots, false) case EmptyTree => // default rethrow clause of try/catch, check tests/patmat/try2.scala Typ(WildcardType, false) + case Block(Nil, expr) => + project(expr) case _ => - ctx.error(s"unknown pattern: $pat", pat.sourcePos) - Empty + // Pattern is an arbitrary expression; assume a skolem (i.e. an unknown value) of the pattern type + Typ(pat.tpe.narrow, false) } private def unapplySeqInfo(resTp: Type, pos: SourcePosition)(implicit ctx: Context): (Int, Type, Type) = { diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index b60e346f6e9a..9b2a07b9cbb4 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -758,23 +758,27 @@ trait Applications extends Compatibility { // lift arguments in the definition order val argDefBuf = mutable.ListBuffer.empty[Tree] typedArgs = lifter.liftArgs(argDefBuf, methType, typedArgs) - // Lifted arguments ordered based on the original order of typedArgBuf and // with all non-explicit default parameters at the end in declaration order. val orderedArgDefs = { - // List of original arguments that are lifted by liftArgs - val impureArgs = typedArgBuf.filterNot(lifter.noLift) - // Assuming stable sorting all non-explicit default parameters will remain in the end with the same order - val defaultParamIndex = args.size - // Mapping of index of each `liftable` into original args ordering - val indices = impureArgs.map { arg => - val idx = args.indexOf(arg) - if (idx >= 0) idx // original index skipping pure arguments - else defaultParamIndex + // Indices of original typed arguments that are lifted by liftArgs + val impureArgIndices = typedArgBuf.zipWithIndex.collect { + case (arg, idx) if !lifter.noLift(arg) => idx + } + def position(arg: Trees.Tree[T]) = { + val i = args.indexOf(arg) + if (i >= 0) i else orderedArgs.length } - scala.util.Sorting.stableSort[(Tree, Int), Int](argDefBuf zip indices, x => x._2).map(_._1) + // The original indices of all ordered arguments, as an array + val originalIndices = orderedArgs.map(position).toArray + // Assuming stable sorting all non-explicit default parameters will remain in the end with the same order + val defaultParamIndex = typedArgs.size + // A map from lifted argument index to the corresponding position in the original argument list + def originalIndex(n: Int) = + if (n < originalIndices.length) originalIndices(n) else orderedArgs.length + scala.util.Sorting.stableSort[(Tree, Int), Int]( + argDefBuf.zip(impureArgIndices), (arg, idx) => originalIndex(idx)).map(_._1) } - liftedDefs ++= orderedArgDefs } if (sameSeq(typedArgs, args)) // trick to cut down on tree copying diff --git a/compiler/src/dotty/tools/dotc/typer/Inliner.scala b/compiler/src/dotty/tools/dotc/typer/Inliner.scala index fc219f485df5..b7ac128dc517 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inliner.scala @@ -38,15 +38,20 @@ object Inliner { def hasBodyToInline(sym: SymDenotation)(implicit ctx: Context): Boolean = sym.isInlineMethod && sym.hasAnnotation(defn.BodyAnnot) - /** The body to inline for method `sym`. + /** The body to inline for method `sym`, or `EmptyTree` if none exists. + * Note: definitions coming from Scala2x class files might be `@forceInline`, + * but still lack that body. * @pre hasBodyToInline(sym) */ def bodyToInline(sym: SymDenotation)(implicit ctx: Context): Tree = - sym.unforcedAnnotation(defn.BodyAnnot).get.tree + if (sym.isInlineMethod && sym.hasAnnotation(defn.BodyAnnot)) + sym.getAnnotation(defn.BodyAnnot).get.tree + else + EmptyTree /** Should call to method `meth` be inlined in this context? */ def isInlineable(meth: Symbol)(implicit ctx: Context): Boolean = - meth.is(Inline) && hasBodyToInline(meth) && !ctx.inInlineMethod + meth.is(Inline) && !ctx.inInlineMethod && !bodyToInline(meth).isEmpty /** Should call be inlined in this context? */ def isInlineable(tree: Tree)(implicit ctx: Context): Boolean = tree match { @@ -105,8 +110,7 @@ object Inliner { cpy.Block(tree)(bindings.toList, inlineCall(tree1)) else if (enclosingInlineds.length < ctx.settings.XmaxInlines.value) { val body = bodyToInline(tree.symbol) // can typecheck the tree and thereby produce errors - if (ctx.reporter.hasErrors) tree - else new Inliner(tree, body).inlined(tree.sourcePos) + new Inliner(tree, body).inlined(tree.sourcePos) } else errorTree( @@ -204,7 +208,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(implicit ctx: Context) { private val inlineCallPrefix = qualifier(methPart).orElse(This(inlinedMethod.enclosingClass.asClass)) - inlining.println("-----------------------\nInlining $call\nWith RHS $rhsToInline") + inlining.println(i"-----------------------\nInlining $call\nWith RHS $rhsToInline") // Make sure all type arguments to the call are fully determined for (targ <- callTypeArgs) fullyDefinedType(targ.tpe, "inlined type argument", targ.span) @@ -421,7 +425,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(implicit ctx: Context) { // Compute bindings for all this-proxies, appending them to bindingsBuf computeThisBindings() - val inlineTyper = new InlineTyper + val inlineTyper = new InlineTyper(ctx.reporter.errorCount) val inlineCtx = inlineContext(call).fresh.setTyper(inlineTyper).setNewScope @@ -986,7 +990,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(implicit ctx: Context) { * 4. Make sure inlined code is type-correct. * 5. Make sure that the tree's typing is idempotent (so that future -Ycheck passes succeed) */ - class InlineTyper extends ReTyper { + class InlineTyper(initialErrorCount: Int) extends ReTyper { import reducer._ override def ensureAccessible(tpe: Type, superAccess: Boolean, pos: SourcePosition)(implicit ctx: Context): Type = { @@ -1033,7 +1037,11 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(implicit ctx: Context) { override def typedApply(tree: untpd.Apply, pt: Type)(implicit ctx: Context): Tree = constToLiteral(betaReduce(super.typedApply(tree, pt))) match { - case res: Apply if res.symbol == defn.InternalQuoted_exprSplice && level == 0 && call.symbol.is(Macro) => + case res: Apply + if res.symbol == defn.InternalQuoted_exprSplice && + level == 0 && + call.symbol.is(Macro) && + !suppressInline => expandMacro(res.args.head, tree.span) case res => res } @@ -1082,7 +1090,11 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(implicit ctx: Context) { } } - override def newLikeThis: Typer = new InlineTyper + override def newLikeThis: Typer = new InlineTyper(initialErrorCount) + + /** Suppress further inlining if this inline typer has already issued errors */ + override def suppressInline given (ctx: Context) = + ctx.reporter.errorCount > initialErrorCount || super.suppressInline } /** Drop any side-effect-free bindings that are unused in expansion or other reachable bindings. @@ -1202,8 +1214,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(implicit ctx: Context) { } } val normalizedSplice = inlinedNormailizer.transform(evaluatedSplice) - - if (ctx.reporter.hasErrors) EmptyTree + if (normalizedSplice.isEmpty) normalizedSplice else normalizedSplice.withSpan(span) } } diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index bf15516c257f..b2585dc7e591 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -805,8 +805,6 @@ class Namer { typer: Typer => private def addInlineInfo(sym: Symbol) = original match { case original: untpd.DefDef if sym.isInlineMethod => - if (sym.owner.isClass && sym.owner.seesOpaques) - ctx.error(em"Implementation restriction: No inline methods allowed where opaque type aliases are in scope", sym.sourcePos) PrepareInlineable.registerInlineInfo( sym, implicit ctx => typedAheadExpr(original).asInstanceOf[tpd.DefDef].rhs diff --git a/compiler/src/dotty/tools/dotc/typer/PrepareInlineable.scala b/compiler/src/dotty/tools/dotc/typer/PrepareInlineable.scala index 50252f08bbd0..2df161364bac 100644 --- a/compiler/src/dotty/tools/dotc/typer/PrepareInlineable.scala +++ b/compiler/src/dotty/tools/dotc/typer/PrepareInlineable.scala @@ -222,39 +222,42 @@ object PrepareInlineable { val inlineCtx = ctx inlined.updateAnnotation(LazyBodyAnnotation { _ => implicit val ctx = inlineCtx - val rawBody = treeExpr(ctx) - val typedBody = - if (ctx.reporter.hasErrors) rawBody - else ctx.compilationUnit.inlineAccessors.makeInlineable(rawBody) - checkInlineMethod(inlined, typedBody) - val inlineableBody = typedBody - inlining.println(i"Body to inline for $inlined: $inlineableBody") - inlineableBody + val initialErrorCount = ctx.reporter.errorCount + var inlinedBody = treeExpr(ctx) + if (ctx.reporter.errorCount == initialErrorCount) { + inlinedBody = ctx.compilationUnit.inlineAccessors.makeInlineable(inlinedBody) + checkInlineMethod(inlined, inlinedBody) + if (ctx.reporter.errorCount != initialErrorCount) + inlinedBody = EmptyTree + } + inlining.println(i"Body to inline for $inlined: $inlinedBody") + inlinedBody }) } } def checkInlineMethod(inlined: Symbol, body: Tree)(implicit ctx: Context): Unit = { + if (inlined.owner.isClass && inlined.owner.seesOpaques) + ctx.error(em"Implementation restriction: No inline methods allowed where opaque type aliases are in scope", inlined.sourcePos) if (ctx.outer.inInlineMethod) ctx.error(ex"implementation restriction: nested inline methods are not supported", inlined.sourcePos) if (inlined.name.isUnapplyName && tupleArgs(body).isEmpty) ctx.warning( em"inline unapply method can be rewritten only if its right hand side is a tuple (e1, ..., eN)", body.sourcePos) - } + if (inlined.is(Macro) && !ctx.isAfterTyper) { - def checkInlineMacro(sym: Symbol, rhs: Tree, pos: SourcePosition)(implicit ctx: Context) = - if (sym.is(Macro) && !ctx.isAfterTyper) { - def isValidMacro(tree: Tree)(implicit ctx: Context): Unit = tree match { + def checkMacro(tree: Tree): Unit = tree match { case Spliced(code) => + if (code.symbol.flags.is(Inline)) + ctx.error("Macro cannot be implemented with an `inline` method", code.sourcePos) Splicer.checkValidMacroBody(code) - new PCPCheckAndHeal(freshStagingContext).transform(rhs) // Ignore output, only check PCP - - case Block(List(stat), Literal(Constants.Constant(()))) => isValidMacro(stat) - case Block(Nil, expr) => isValidMacro(expr) - case Typed(expr, _) => isValidMacro(expr) + new PCPCheckAndHeal(freshStagingContext).transform(body) // Ignore output, only check PCP + case Block(List(stat), Literal(Constants.Constant(()))) => checkMacro(stat) + case Block(Nil, expr) => checkMacro(expr) + case Typed(expr, _) => checkMacro(expr) case Block(DefDef(nme.ANON_FUN, _, _, _, _) :: Nil, Closure(_, fn, _)) if fn.symbol.info.isImplicitMethod => - // TODO Suppot this pattern + // TODO Support this pattern ctx.error( """Macros using a return type of the form `foo(): given X => Y` are not yet supported. | @@ -269,9 +272,9 @@ object PrepareInlineable { | | * The contents of the splice must call a static method | * All arguments must be quoted or inline - """.stripMargin, pos) + """.stripMargin, inlined.sourcePos) } - isValidMacro(rhs) + checkMacro(body) } + } } - diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 826e2e74e7e8..a3dc49dd3c72 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -111,6 +111,7 @@ class Typer extends Namer */ private[this] var foundUnderScala2: Type = NoType + // Overridden in derived typers def newLikeThis: Typer = new Typer /** Find the type of an identifier with given `name` in given context `ctx`. @@ -507,6 +508,70 @@ class Typer extends Namer } } + def typedNumber(tree: untpd.Number, pt: Type)(implicit ctx: Context): Tree = { + import scala.util.FromDigits._ + import untpd.NumberKind._ + record("typedNumber") + val digits = tree.digits + val target = pt.dealias + def lit(value: Any) = Literal(Constant(value)).withSpan(tree.span) + try { + // Special case primitive numeric types + if (target.isRef(defn.IntClass) || + target.isRef(defn.CharClass) || + target.isRef(defn.ByteClass) || + target.isRef(defn.ShortClass)) + tree.kind match { + case Whole(radix) => return lit(intFromDigits(digits, radix)) + case _ => + } + else if (target.isRef(defn.LongClass)) + tree.kind match { + case Whole(radix) => return lit(longFromDigits(digits, radix)) + case _ => + } + else if (target.isRef(defn.FloatClass)) + return lit(floatFromDigits(digits)) + else if (target.isRef(defn.DoubleClass)) + 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 firstArg = Literal(Constant(digits)) + 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) + case _ => + } + } + // Otherwise convert to Int or Double according to digits format + tree.kind match { + case Whole(radix) => lit(intFromDigits(digits, radix)) + case _ => lit(doubleFromDigits(digits)) + } + } + catch { + case ex: FromDigitsException => + ctx.error(ex.getMessage, tree.sourcePos) + tree.kind match { + case Whole(_) => lit(0) + case _ => lit(0.0) + } + } + } + def typedLiteral(tree: untpd.Literal)(implicit ctx: Context): Tree = { val tree1 = assignType(tree) if (ctx.mode.is(Mode.Type)) tpd.SingletonTypeTree(tree1) // this ensures that tree is classified as a type tree @@ -701,7 +766,8 @@ class Typer extends Namer (index(stats), typedStats(stats, ctx.owner)) def typedBlock(tree: untpd.Block, pt: Type)(implicit ctx: Context): Tree = { - val (exprCtx, stats1) = typedBlockStats(tree.stats) + val localCtx = ctx.retractMode(Mode.Pattern) + val (exprCtx, stats1) = typedBlockStats(tree.stats) given localCtx val expr1 = typedExpr(tree.expr, pt.dropIfProto)(exprCtx) ensureNoLocalRefs( cpy.Block(tree)(stats1, expr1).withType(expr1.tpe), pt, localSyms(stats1)) @@ -1536,10 +1602,8 @@ class Typer extends Namer if (sym.isInlineMethod) rhsCtx.addMode(Mode.InlineableBody) val rhs1 = typedExpr(ddef.rhs, tpt1.tpe.widenExpr)(rhsCtx) - if (sym.isInlineMethod) { - PrepareInlineable.checkInlineMacro(sym, rhs1, ddef.sourcePos) + if (sym.isInlineMethod) PrepareInlineable.registerInlineInfo(sym, _ => rhs1) - } if (sym.isConstructor && !sym.isPrimaryConstructor) { for (param <- tparams1 ::: vparamss1.flatten) @@ -1998,6 +2062,7 @@ class Typer extends Namer case tree: untpd.Apply => if (ctx.mode is Mode.Pattern) typedUnApply(tree, pt) else typedApply(tree, pt) case tree: untpd.This => typedThis(tree) + case tree: untpd.Number => typedNumber(tree, pt) case tree: untpd.Literal => typedLiteral(tree) case tree: untpd.New => typedNew(tree, pt) case tree: untpd.Typed => typedTyped(tree, pt) @@ -2119,7 +2184,7 @@ class Typer extends Namer traverse(xtree :: rest) case none => typed(mdef) match { - case mdef1: DefDef if Inliner.hasBodyToInline(mdef1.symbol) => + case mdef1: DefDef if !Inliner.bodyToInline(mdef1.symbol).isEmpty => buf += inlineExpansion(mdef1) // replace body with expansion, because it will be used as inlined body // from separately compiled files - the original BodyAnnotation is not kept. @@ -2666,10 +2731,11 @@ class Typer extends Namer } else if (Inliner.isInlineable(tree) && !ctx.settings.YnoInline.value && - !ctx.isAfterTyper && - !ctx.reporter.hasErrors) { + !suppressInline) { tree.tpe <:< wildApprox(pt) - readaptSimplified(Inliner.inlineCall(tree)) + val errorCount = ctx.reporter.errorCount + val inlined = Inliner.inlineCall(tree) + if (errorCount == ctx.reporter.errorCount) readaptSimplified(inlined) else inlined } else if (tree.symbol.isScala2Macro && // raw and s are eliminated by the StringInterpolatorOpt phase @@ -2978,6 +3044,9 @@ class Typer extends Namer } } + // Overridden in InlineTyper + def suppressInline given (ctx: Context): Boolean = ctx.isAfterTyper + /** Does the "contextuality" of the method type `methType` match the one of the prototype `pt`? * This is the case if * - both are contextual, or diff --git a/docs/docs/internals/syntax.md b/docs/docs/internals/syntax.md index fbd6479b3f1c..1d8888698c58 100644 --- a/docs/docs/internals/syntax.md +++ b/docs/docs/internals/syntax.md @@ -261,8 +261,7 @@ TypeCaseClauses ::= TypeCaseClause { TypeCaseClause } TypeCaseClause ::= ‘case’ InfixType ‘=>’ Type [nl] Pattern ::= Pattern1 { ‘|’ Pattern1 } Alternative(pats) -Pattern1 ::= PatVar ‘:’ RefinedType Bind(name, Typed(Ident(wildcard), tpe)) - | Pattern2 +Pattern1 ::= Pattern2 [‘:’ RefinedType] Bind(name, Typed(Ident(wildcard), tpe)) Pattern2 ::= [id ‘@’] InfixPattern Bind(name, pat) InfixPattern ::= SimplePattern { id [cnl] SimplePattern } InfixOp(pat, op, pat) SimplePattern ::= PatVar Ident(wildcard) diff --git a/docs/docs/reference/changed-features/numeric-literals.md b/docs/docs/reference/changed-features/numeric-literals.md new file mode 100644 index 000000000000..6be9c6e49473 --- /dev/null +++ b/docs/docs/reference/changed-features/numeric-literals.md @@ -0,0 +1,245 @@ +--- +layout: doc-page +title: Numeric Literals +--- + +In Scala 2, numeric literals were confined to the primitive numeric types `Int`, Long`, `Float`, and `Double`. Scala 3 allows to write numeric literals also for user defined types. Example: +``` +val x: Long = -10_000_000_000 +val y: BigInt = 0x123_abc_789_def_345_678_901 +val z: BigDecimal = 110_222_799_799.99 + +(y: BigInt) match { + case 123_456_789_012_345_678_901 => +} +``` +The syntax of numeric literals is the same as before, except there are no pre-set limits +how large they can be. + +### Meaning of Numeric Literals + +The meaning of a numeric literal is determined as follows: + + - If the literal ends with `l` or `L`, it is a `Long` integer (and must fit + in its legal range). + - If the literal ends with `f` or `F`, it is a single precision floating point number of type `Float`. + - If the literal ends with `d` or `D`, it is a double precision floating point number of type `Double`. + +In each of these cases the conversion to a number is exactly as in Scala 2 or in Java. If a numeric literal does _not_ end in one of these suffixes, its meaning is determined by the expected type: + + 1. If the expected type is `Int`, `Long`, `Float`, or `Double`, the literal is + treated as a standard literal of that type. + 2. If the expected type is a fully defined type `T` that has a given instance of type + `scala.util.FromDigits[T]`, the literal is converted to a value of type `T` by passing it as an argument to + the `fromDigits` method of that instance (more details below). + 3. Otherwise, the literal is treated as a `Double` literal (if it has a decimal point or an + exponent), or as an `Int` literal (if not). (This last possibility is again as in Scala 2 or Java.) + +With these rules, the definition +```scala +val x: Long = -10_000_000_000 +``` +is legal by rule (1), since the expected type is `Long`. The definitions +```scala +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). +On the other hand, +```scala +val x = -10_000_000_000 +``` +gives a type error, since without an expected type `-10_000_000_000` is treated by rule (3) as an `Int` literal, but it is too large for that type. + +### The FromDigits Class + +To allow numeric literals, a type simply has to define a given instance of the +`scala.util.FromDigits` typeclass, or one of its subclasses. `FromDigits` is defined +as follows: +```scala +trait FromDigits[T] { + def fromDigits(digits: String): T +} +``` +Implementations of the `fromDigits` convert strings of digits to the values of the +implementation type `T`. +The `digits` string consists of digits between `0` and `9`, possibly preceded by a +sign ("+" or "-"). Number separator characters `_` are filtered out before +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: +```scala +object FromDigits { + + /** A subclass of `FromDigits` that also allows to convert whole number literals + * with a radix other than 10 + */ + trait WithRadix[T] extends FromDigits[T] { + def fromDigits(digits: String): T = fromDigits(digits, 10) + def fromDigits(digits: String, radix: Int): T + } + + /** A subclass of `FromDigits` that also allows to convert number + * literals containing 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*`. + */ + trait Floating[T] extends Decimal[T] + ... +} +``` +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. + +### Error Handling + +`FromDigits` implementations can signal errors by throwing exceptions of some subtype +of `FromDigitsException`. `FromDigitsException` is defined with three subclasses in the +`FromDigits` object as follows: +```scala + abstract class FromDigitsException(msg: String) extends NumberFormatException(msg) + + class NumberTooLarge (msg: String = "number too large") extends FromDigitsException(msg) + class NumberTooSmall (msg: String = "number too small") extends FromDigitsException(msg) + class MalformedNumber(msg: String = "malformed number literal") extends FromDigitsException(msg) +``` + +### 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: +```scala +case class BigFloat(mantissa: BigInt, exponent: Int) { + override def toString = s"${mantissa}e${exponent}" +} +``` +`BigFloat` literals can have a decimal point as well as an exponent. E.g. the following expression +should produce the `BigFloat` number `BigFloat(-123, 997)`: +```scala +-0.123E+1000: BigFloat +``` +The companion object of `BigFloat` defines an `apply` constructor method to construct a `BigFloat` +from a `digits` string. Here is a possible implementation: +```scala +object BigFloat { + import scala.util.FromDigits + + def apply(digits: String): BigFloat = { + val (mantissaDigits, givenExponent) = digits.toUpperCase.split('E') match { + case Array(mantissaDigits, edigits) => + val expo = + try FromDigits.intFromDigits(edigits) + catch { + case ex: FromDigits.NumberTooLarge => + throw FromDigits.NumberTooLarge(s"exponent too large: $edigits") + } + (mantissaDigits, expo) + case Array(mantissaDigits) => + (mantissaDigits, 0) + } + val (intPart, exponent) = mantissaDigits.split('.') match { + case Array(intPart, decimalPart) => + (intPart ++ decimalPart, givenExponent - decimalPart.length) + case Array(intPart) => + (intPart, givenExponent) + } + BigFloat(BigInt(intPart), exponent) + } +``` +To accept `BigFloat` literals, all that's needed in addition is a given instance of type +`FromDigits.Floating[BigFloat]`: +```scala + given FromDigits as FromDigits.Floating[BigFloat] { + def fromDigits(digits: String) = apply(digits) + } +} // end BigFloat +``` +Note that the `apply` method does not check the format of the `digits` argument. It is +assumed that only valid arguments are passed. For calls coming from the compiler +that assumption is valid, since the compiler will first check whether a numeric +literal has the correct format before it gets passed on to a conversion method. + +### Compile-Time Errors + +With the setup of the previous section, a literal like +```scala +1e10_0000_000_000: BigFloat +``` +would be expanded by the compiler to +```scala +BigFloat.FromDigits.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 +with a small dose of meta-programming. The idea is to turn the `fromDigits` method +into a macro, i.e. make it an inline method with a splice as right hand side. +To do this, replace the `FromDigits` instance in the `BigFloat` object by the following two definitions: +```scala +object BigFloat { + ... + + class FromDigits extends FromDigits.Floating[BigFloat] { + def fromDigits(digits: String) = apply(digits) + } + + given as FromDigits { + override inline def fromDigits(digits: String) = ${ + fromDigitsImpl('digits) + } + } +``` +Note that an inline method cannot directly fill in for an abstract method, since it produces +no code that can be executed at runtime. That's why we define an intermediary class +`FromDigits` that contains a fallback implementation which is then overridden by the inline +method in the `FromDigits` given instance. That method is defined in terms of a macro +implementation method `fromDigitsImpl`. Here is its definition: +```scala + private def fromDigitsImpl(digits: Expr[String]) given (ctx: QuoteContext): Expr[BigFloat] = + digits match { + case Const(ds) => + try { + val BigFloat(m, e) = apply(ds) + '{BigFloat(${m.toExpr}, ${e.toExpr})} + } + catch { + case ex: FromDigits.FromDigitsException => + ctx.error(ex.getMessage) + '{BigFloat(0, 0)} + } + case digits => + '{apply($digits)} + } +} // end BigFloat +``` +The macro implementation takes an argument of type `Expr[String]` and yields +a result of type `Expr[BigFloat]`. It tests whether its argument is a constant +string. If that's the case, it converts the string using the `apply` method +and lifts the resulting `BigFloat` back to `Expr` level. For non-constant +strings `fromDigitsImpl(digits)` is simply `apply(digits)`, i.e. everything is +evaluated at runtime in this case. + +The interesting part is the `catch` part of the case where `digits` is constant. +If the `apply` method throws a `FromDigitsException`, the exception's message is issued as a compile time error +in the `ctx.error(ex.getMessage)` call. + +With this new implementation, a definition like +```scala + val x: BigFloat = 1234.45e3333333333 +``` +would give a compile time error message: +```scala +3 | val x: BigFloat = 1234.45e3333333333 + | ^^^^^^^^^^^^^^^^^^ + | exponent too large: 3333333333 +``` + + + + diff --git a/docs/docs/reference/contextual/given-clauses.md b/docs/docs/reference/contextual/given-clauses.md index 8ecc479cfa8b..020fa95ef7e9 100644 --- a/docs/docs/reference/contextual/given-clauses.md +++ b/docs/docs/reference/contextual/given-clauses.md @@ -13,7 +13,7 @@ For example, with the [given instances](./delegates.md) defined previously, a maximum function that works for any arguments for which an ordering exists can be defined as follows: ```scala def max[T](x: T, y: T) given (ord: Ord[T]): T = - if (ord.compare(x, y) < 1) y else x + if (ord.compare(x, y) < 0) y else x ``` Here, `ord` is an _implicit parameter_ introduced with a `given` clause. The `max` method can be applied as follows: diff --git a/docs/sidebar.yml b/docs/sidebar.yml index 62c1f3d1825c..33e9921a6bb9 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -107,6 +107,8 @@ sidebar: url: docs/reference/other-new-features/indentation.html - title: Other Changed Features subsection: + - title: Numeric Literals + url: docs/reference/changed-features/numeric-literals.html - title: Structural Types url: docs/reference/changed-features/structural-types.html - title: Operators diff --git a/library/src/scala/util/FromDigits.scala b/library/src/scala/util/FromDigits.scala new file mode 100644 index 000000000000..62e9843c56bd --- /dev/null +++ b/library/src/scala/util/FromDigits.scala @@ -0,0 +1,164 @@ +package scala.util +import scala.math.{BigInt} +import quoted._ +import quoted.matching._ +import internal.Chars.digit2int +import annotation.internal.sharable + +/** A typeclass for types that admit numeric literals. + */ +trait FromDigits[T] { + + /** Convert `digits` string to value of type `T` + * `digits` can contain + * - sign `+` or `-` + * - sequence of digits between 0 and 9 + * + * @throws MalformedNumber if digit string is not legal for the given type + * @throws NumberTooLarge if value of result does not fit into `T`'s range + * @throws NumberTooSmall in case of numeric underflow (e.g. a non-zero + * floating point literal that produces a zero value) + */ + def fromDigits(digits: String): T +} + +object FromDigits { + + /** A subclass of `FromDigits` that also allows to convert whole number literals + * with a radix other than 10 + */ + trait WithRadix[T] extends FromDigits[T] { + def fromDigits(digits: String): T = fromDigits(digits, 10) + + /** Convert digits string with given radix to numberof type `T`. + * E.g. if radix is 16, digits `a..f` and `A..F` are also allowed. + */ + def fromDigits(digits: String, radix: Int): T + } + + /** A subclass of `FromDigits` that also allows to convert number + * literals containing 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*`. + */ + trait Floating[T] extends Decimal[T] + + /** The base type for exceptions that can be thrown from + * `fromDigits` conversions + */ + abstract class FromDigitsException(msg: String) extends NumberFormatException(msg) + + /** Thrown if value of result does not fit into result type's range */ + class NumberTooLarge(msg: String = "number too large") extends FromDigitsException(msg) + + /** Thrown in case of numeric underflow (e.g. a non-zero + * floating point literal that produces a zero value) + */ + class NumberTooSmall(msg: String = "number too small") extends FromDigitsException(msg) + + /** Thrown if digit string is not legal for the given type */ + class MalformedNumber(msg: String = "malformed number literal") extends FromDigitsException(msg) + + /** Convert digits and radix to integer value (either int or Long) + * This is tricky because of the max negative value. + * Note: We cannot use java.lang.Integer.valueOf or java.lang.Long.valueOf + * since these do not handle unsigned hex numbers greater than the maximal value + * correctly. + */ + private def integerFromDigits(digits: String, radix: Int, limit: Long): Long = { + var value: Long = 0 + val divider = if (radix == 10) 1 else 2 + var i = 0 + var negated = false + val len = digits.length + if (0 < len && (digits(0) == '-' || digits(0) == '+')) { + negated = digits(0) == '-' + i += 1 + } + if (i == len) throw MalformedNumber() + while (i < len) { + val c = digits(i) + val d = digit2int(c, radix) + if (d < 0) throw MalformedNumber() + if (value < 0 || + limit / (radix / divider) < value || + limit - (d / divider) < value * (radix / divider) && + !(negated && limit == value * radix - 1 + d)) throw NumberTooLarge() + value = value * radix + d + i += 1 + } + if (negated) -value else value + } + + /** Convert digit string to Int number + * @param digits The string to convert + * @param radix The radix + * @throws NumberTooLarge if number does not fit within Int range + * @throws MalformedNumber if digits is not a legal digit string. + * Legal strings consist only of digits conforming to radix, + * possibly preceded by a "-" sign. + */ + def intFromDigits(digits: String, radix: Int = 10): Int = + integerFromDigits(digits, radix, Int.MaxValue).toInt + + /** Convert digit string to Long number + * @param digits The string to convert + * @param radix The radix + * @throws NumberTooLarge if the resulting number does not fit within Long range + * @throws MalformedNumber if digits is not a legal digit string. + * Legal strings consist only of digits conforming to radix, + * possibly preceded by a "-" sign. + */ + def longFromDigits(digits: String, radix: Int = 10): Long = + integerFromDigits(digits, radix, Long.MaxValue) + + @sharable private val zeroFloat = raw"-?[0.]+(?:[eE][+-]?[0-9]+)?[fFdD]?".r + + /** Convert digit string to Float number + * @param digits The string to convert + * @throws NumberTooLarge if the resulting number is infinite + * @throws NumberTooSmall if the resulting number is 0.0f, yet the digits + * string contains non-zero digits before the exponent. + * @throws MalformedNumber if digits is not a legal digit string for floating point numbers. + */ + def floatFromDigits(digits: String): Float = { + val x: Float = + try java.lang.Float.parseFloat(digits) + catch { + case ex: NumberFormatException => throw MalformedNumber() + } + if (x.isInfinite) throw NumberTooLarge() + if (x == 0.0f && !zeroFloat.pattern.matcher(digits).matches) throw NumberTooSmall() + x + } + + /** Convert digit string to Double number + * @param digits The string to convert + * @throws NumberTooLarge if the resulting number is infinite + * @throws NumberTooSmall if the resulting number is 0.0d, yet the digits + * string contains non-zero digits before the exponent. + * @throws MalformedNumber if digits is not a legal digit string for floating point numbers.. + */ + def doubleFromDigits(digits: String): Double = { + val x: Double = + try java.lang.Double.parseDouble(digits) + catch { + case ex: NumberFormatException => throw MalformedNumber() + } + if (x.isInfinite) throw NumberTooLarge() + if (x == 0.0d && !zeroFloat.pattern.matcher(digits).matches) throw NumberTooSmall() + x + } + + given BigIntFromDigits as FromDigits.WithRadix[BigInt] { + def fromDigits(digits: String, radix: Int): BigInt = BigInt(digits, radix) + } + + given BigDecimalFromDigits as FromDigits.Decimal[BigDecimal] { + def fromDigits(digits: String): BigDecimal = BigDecimal(digits) + } +} diff --git a/tests/neg-with-compiler/GenericNumLits/Even_1.scala b/tests/neg-with-compiler/GenericNumLits/Even_1.scala new file mode 100644 index 000000000000..be5a8b466381 --- /dev/null +++ b/tests/neg-with-compiler/GenericNumLits/Even_1.scala @@ -0,0 +1,37 @@ +import scala.util.FromDigits +import scala.quoted._ +import scala.quoted.matching._ + +case class Even(n: Int) +object Even { + + private def evenFromDigits(digits: String): Even = { + val intValue = FromDigits.intFromDigits(digits) + if (intValue % 2 == 0) Even(intValue) + else throw FromDigits.MalformedNumber(s"$digits is odd") + } + + private def evenFromDigitsImpl(digits: Expr[String]) given (ctx: QuoteContext): Expr[Even] = digits match { + case Const(ds) => + val ev = + try evenFromDigits(ds) + catch { + case ex: FromDigits.FromDigitsException => + ctx.error(ex.getMessage) + Even(0) + } + '{Even(${ev.n.toExpr})} + case _ => + '{evenFromDigits($digits)} + } + + class EvenFromDigits extends FromDigits[Even] { + def fromDigits(digits: String) = evenFromDigits(digits) + } + + given as EvenFromDigits { + override inline def fromDigits(digits: String) = ${ + evenFromDigitsImpl('digits) + } + } +} diff --git a/tests/neg-with-compiler/GenericNumLits/Test_2.scala b/tests/neg-with-compiler/GenericNumLits/Test_2.scala new file mode 100644 index 000000000000..783ec1bf3e53 --- /dev/null +++ b/tests/neg-with-compiler/GenericNumLits/Test_2.scala @@ -0,0 +1,5 @@ +object Test extends App { + + val e1: Even = 1234 + val e2: Even = 123 // error: 123 is odd +} \ No newline at end of file diff --git a/tests/neg/BigFloat/BigFloat_1.scala b/tests/neg/BigFloat/BigFloat_1.scala new file mode 100644 index 000000000000..ad8439368670 --- /dev/null +++ b/tests/neg/BigFloat/BigFloat_1.scala @@ -0,0 +1,66 @@ +package test +import scala.util.FromDigits +import scala.quoted._ +import scala.quoted.matching._ + +case class BigFloat(mantissa: BigInt, exponent: Int) { + override def toString = s"${mantissa}e${exponent}" +} + +object BigFloat extends App { + def apply(digits: String): BigFloat = { + val (mantissaDigits, givenExponent) = digits.toUpperCase.split('E') match { + case Array(mantissaDigits, edigits) => + val expo = + try FromDigits.intFromDigits(edigits) + catch { + case ex: FromDigits.NumberTooLarge => + throw FromDigits.NumberTooLarge(s"exponent too large: $edigits") + } + (mantissaDigits, expo) + case Array(mantissaDigits) => + (mantissaDigits, 0) + } + val (intPart, exponent) = mantissaDigits.split('.') match { + case Array(intPart, decimalPart) => + (intPart ++ decimalPart, givenExponent - decimalPart.length) + case Array(intPart) => + (intPart, givenExponent) + } + BigFloat(BigInt(intPart), exponent) + } + + private def fromDigitsImpl(digits: Expr[String]) given (ctx: QuoteContext): Expr[BigFloat] = + digits match { + case Const(ds) => + try { + val BigFloat(m, e) = apply(ds) + '{BigFloat(${m.toExpr}, ${e.toExpr})} + } + catch { + case ex: FromDigits.FromDigitsException => + ctx.error(ex.getMessage) + '{BigFloat(0, 0)} + } + case digits => + '{apply($digits)} + } + + class BigFloatFromDigits extends FromDigits.Floating[BigFloat] { + def fromDigits(digits: String) = apply(digits) + } + + given as BigFloatFromDigits { + override inline def fromDigits(digits: String) = ${ + fromDigitsImpl('digits) + } + } + + // Should be in StdLib: + + given as Liftable[BigInt] { + def toExpr(x: BigInt) = + '{BigInt(${x.toString.toExpr})} + } +} + diff --git a/tests/neg/BigFloat/Test_2.scala b/tests/neg/BigFloat/Test_2.scala new file mode 100644 index 000000000000..fa7e6a8380e2 --- /dev/null +++ b/tests/neg/BigFloat/Test_2.scala @@ -0,0 +1,4 @@ +import test.BigFloat +object Test extends App { + val x: BigFloat = 1234.45e3333333333 // error: exponent too large +} \ No newline at end of file diff --git a/tests/neg/GenericNumLits/Even_1.scala b/tests/neg/GenericNumLits/Even_1.scala new file mode 100644 index 000000000000..be5a8b466381 --- /dev/null +++ b/tests/neg/GenericNumLits/Even_1.scala @@ -0,0 +1,37 @@ +import scala.util.FromDigits +import scala.quoted._ +import scala.quoted.matching._ + +case class Even(n: Int) +object Even { + + private def evenFromDigits(digits: String): Even = { + val intValue = FromDigits.intFromDigits(digits) + if (intValue % 2 == 0) Even(intValue) + else throw FromDigits.MalformedNumber(s"$digits is odd") + } + + private def evenFromDigitsImpl(digits: Expr[String]) given (ctx: QuoteContext): Expr[Even] = digits match { + case Const(ds) => + val ev = + try evenFromDigits(ds) + catch { + case ex: FromDigits.FromDigitsException => + ctx.error(ex.getMessage) + Even(0) + } + '{Even(${ev.n.toExpr})} + case _ => + '{evenFromDigits($digits)} + } + + class EvenFromDigits extends FromDigits[Even] { + def fromDigits(digits: String) = evenFromDigits(digits) + } + + given as EvenFromDigits { + override inline def fromDigits(digits: String) = ${ + evenFromDigitsImpl('digits) + } + } +} diff --git a/tests/neg/GenericNumLits/Test_2.scala b/tests/neg/GenericNumLits/Test_2.scala new file mode 100644 index 000000000000..9ba8aec01b9e --- /dev/null +++ b/tests/neg/GenericNumLits/Test_2.scala @@ -0,0 +1,6 @@ +object Test extends App { + + val e1: Even = 1234 + val e2: Even = 123 // error: 123 is odd + val e3: Even = 123456789101111 // error: number too large +} \ No newline at end of file diff --git a/tests/neg/t6124.check b/tests/neg/t6124.check index a03500d944a8..7368a40c9131 100644 --- a/tests/neg/t6124.check +++ b/tests/neg/t6124.check @@ -30,10 +30,6 @@ 10 | def s = 3_.1 // error | ^ | trailing separator is not allowed --- Error: tests/neg/t6124.scala:12:17 ---------------------------------------------------------------------------------- -12 | def tooSmall = 1.0E-325 // error - | ^ - | double precision floating point number too small -- Error: tests/neg/t6124.scala:17:13 ---------------------------------------------------------------------------------- 17 | val pi1 = 3_.1415F // error | ^ @@ -58,6 +54,10 @@ 26 | val x8 = 0x52_ // error | ^ | trailing separator is not allowed +-- Error: tests/neg/t6124.scala:12:17 ---------------------------------------------------------------------------------- +12 | def tooSmall = 1.0E-325 // error + | ^^^^^^^^ + | number too small -- [E008] Member Not Found Error: tests/neg/t6124.scala:18:14 ---------------------------------------------------------- 18 | val pi2 = 3._1415F // error | ^^^^^^^^ diff --git a/tests/pos/patmat.scala b/tests/pos/patmat.scala index 94d4580c5267..c55308d9810a 100644 --- a/tests/pos/patmat.scala +++ b/tests/pos/patmat.scala @@ -8,7 +8,7 @@ object Test { xs.length match { case 0 => println("0") - case 1 => println("1") + case 1: Int => println("1") case 2 => println("2") case 3 => println("3") case 4 => println("4") @@ -16,7 +16,7 @@ object Test { } (xs.length, xs) match { - case (0, Nil) => println("1") + case (0, Nil: List[Int]) => println("1") case (_, Nil) => println("2") case (0, _) => println("3") case (x, y) => println("4") @@ -46,4 +46,9 @@ object Test { case Some(s) => println(s) case None => println("nothing") } + + type IntPair = (Int, Int) + ??? match { + case (x, y): IntPair => x * y + } } diff --git a/tests/run-with-compiler/BigFloat.check b/tests/run-with-compiler/BigFloat.check new file mode 100644 index 000000000000..b5c00805e346 --- /dev/null +++ b/tests/run-with-compiler/BigFloat.check @@ -0,0 +1,5 @@ +123445e-2 +123445678e-5 +123445e1 +-123e997 +too large diff --git a/tests/run-with-compiler/BigFloat/BigFloat_1.scala b/tests/run-with-compiler/BigFloat/BigFloat_1.scala new file mode 100644 index 000000000000..ad8439368670 --- /dev/null +++ b/tests/run-with-compiler/BigFloat/BigFloat_1.scala @@ -0,0 +1,66 @@ +package test +import scala.util.FromDigits +import scala.quoted._ +import scala.quoted.matching._ + +case class BigFloat(mantissa: BigInt, exponent: Int) { + override def toString = s"${mantissa}e${exponent}" +} + +object BigFloat extends App { + def apply(digits: String): BigFloat = { + val (mantissaDigits, givenExponent) = digits.toUpperCase.split('E') match { + case Array(mantissaDigits, edigits) => + val expo = + try FromDigits.intFromDigits(edigits) + catch { + case ex: FromDigits.NumberTooLarge => + throw FromDigits.NumberTooLarge(s"exponent too large: $edigits") + } + (mantissaDigits, expo) + case Array(mantissaDigits) => + (mantissaDigits, 0) + } + val (intPart, exponent) = mantissaDigits.split('.') match { + case Array(intPart, decimalPart) => + (intPart ++ decimalPart, givenExponent - decimalPart.length) + case Array(intPart) => + (intPart, givenExponent) + } + BigFloat(BigInt(intPart), exponent) + } + + private def fromDigitsImpl(digits: Expr[String]) given (ctx: QuoteContext): Expr[BigFloat] = + digits match { + case Const(ds) => + try { + val BigFloat(m, e) = apply(ds) + '{BigFloat(${m.toExpr}, ${e.toExpr})} + } + catch { + case ex: FromDigits.FromDigitsException => + ctx.error(ex.getMessage) + '{BigFloat(0, 0)} + } + case digits => + '{apply($digits)} + } + + class BigFloatFromDigits extends FromDigits.Floating[BigFloat] { + def fromDigits(digits: String) = apply(digits) + } + + given as BigFloatFromDigits { + override inline def fromDigits(digits: String) = ${ + fromDigitsImpl('digits) + } + } + + // Should be in StdLib: + + given as Liftable[BigInt] { + def toExpr(x: BigInt) = + '{BigInt(${x.toString.toExpr})} + } +} + diff --git a/tests/run-with-compiler/BigFloat/Test_2.scala b/tests/run-with-compiler/BigFloat/Test_2.scala new file mode 100644 index 000000000000..cae28d1ab700 --- /dev/null +++ b/tests/run-with-compiler/BigFloat/Test_2.scala @@ -0,0 +1,20 @@ +import test.BigFloat +import scala.util.FromDigits +object Test extends App { + + + println(BigFloat("1234.45")) + println(BigFloat("1234.45" ++ "678")) + println(BigFloat("1234.45e3")) + println(BigFloat("-0.123E+1000")) + try println(BigFloat("1234.45e3333333333")) + catch { + case ex: FromDigits.FromDigitsException => println("too large") + } + + def check(x: BigFloat, digits: String) = + assert(x == BigFloat(digits), x) + + check(1234.45, "1234.45") + check(1234.45e3, "1234.45e3") +} \ No newline at end of file diff --git a/tests/run/genericNumLits.check b/tests/run/genericNumLits.check new file mode 100644 index 000000000000..9376befb1c30 --- /dev/null +++ b/tests/run/genericNumLits.check @@ -0,0 +1,5 @@ +13232202002020202020202 +-50390822187678765893036 +132322020020.223 +Even(1234) +malformed diff --git a/tests/run/genericNumLits.scala b/tests/run/genericNumLits.scala new file mode 100644 index 000000000000..6cb657d692da --- /dev/null +++ b/tests/run/genericNumLits.scala @@ -0,0 +1,57 @@ +import scala.util.FromDigits +object Test extends App { + + val x: BigInt = 13232202002020202020202 + val y: BigInt = -0xaabb12345ACF12345AC + val z: BigDecimal = 132322020020.223 + + case class Even(n: Int) + + given as FromDigits[Even] { + def fromDigits(digits: String): Even = { + val intValue = digits.toInt + if (intValue % 2 == 0) Even(intValue) + else throw FromDigits.MalformedNumber() + } + } + + val e: Even = 1234 + + println(x) + println(y) + println(z) + println(e) + + try println(123: Even) + catch { + case ex: FromDigits.MalformedNumber => println("malformed") + } + + val N = 10 + + x match { + case 13_232_202_002_020_202_020_202 => () + } + (x: Any) match { + case 13232202002020202020202: BigInt => () + } + y match { + case 13232202002020202020202 => assert(false) + case -0xaabb12345ACF12345AC => () + } + z match { + case 132322020020.223 => () + } + (z: Any) match { + case 132_322_020_020.223: BigDecimal => () + } + + e match { + case 1234 => + } + (e: Any) match { + case 12: Even => assert(false) + case 1234: Even => + case _: Even => + } +} \ No newline at end of file