diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index f83873ce4b26..024d313e069c 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -98,9 +98,9 @@ object Feature: else false - def checkExperimentalFeature(which: String, srcPos: SrcPos)(using Context) = + def checkExperimentalFeature(which: String, srcPos: SrcPos, note: => String = "")(using Context) = if !isExperimentalEnabled then - report.error(i"Experimental $which may only be used with a nightly or snapshot version of the compiler", srcPos) + report.error(i"Experimental $which may only be used with a nightly or snapshot version of the compiler$note", srcPos) def checkExperimentalDef(sym: Symbol, srcPos: SrcPos)(using Context) = if !isExperimentalEnabled then diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 9e205b6a8c01..27c1dead4482 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -3114,10 +3114,6 @@ object Parsers { languageImport(tree) match case Some(prefix) => in.languageImportContext = in.languageImportContext.importContext(imp, NoSymbol) - if prefix == nme.experimental - && selectors.exists(sel => Feature.experimental(sel.name) != Feature.scala2macros && Feature.experimental(sel.name) != Feature.erasedDefinitions) - then - Feature.checkExperimentalFeature("features", imp.srcPos) for case ImportSelector(id @ Ident(imported), EmptyTree, _) <- selectors if allSourceVersionNames.contains(imported) diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index da386525511a..c7e02a5c6837 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -448,6 +448,10 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase throw ex } + override def transformStats(trees: List[Tree], exprOwner: Symbol)(using Context): List[Tree] = + try super.transformStats(trees, exprOwner) + finally Checking.checkExperimentalImports(trees) + /** Transforms the rhs tree into a its default tree if it is in an `erased` val/def. * Performed to shrink the tree that is known to be erased later. */ diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 850c104556aa..3b743906fd51 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -721,6 +721,50 @@ object Checking { checkValue(tree) case _ => tree + + /** Check that experimental language imports in `trees` + * are done only in experimental scopes, or in a top-level + * scope with only @experimental definitions. + */ + def checkExperimentalImports(trees: List[Tree])(using Context): Unit = + + def nonExperimentalStat(trees: List[Tree]): Tree = trees match + case (_: Import | EmptyTree) :: rest => + nonExperimentalStat(rest) + case (tree @ TypeDef(_, impl: Template)) :: rest if tree.symbol.isPackageObject => + nonExperimentalStat(impl.body).orElse(nonExperimentalStat(rest)) + case (tree: PackageDef) :: rest => + nonExperimentalStat(tree.stats).orElse(nonExperimentalStat(rest)) + case (tree: MemberDef) :: rest => + if tree.symbol.isExperimental || tree.symbol.is(Synthetic) then + nonExperimentalStat(rest) + else + tree + case tree :: rest => + tree + case Nil => + EmptyTree + + for case imp @ Import(qual, selectors) <- trees do + def isAllowedImport(sel: untpd.ImportSelector) = + val name = Feature.experimental(sel.name) + name == Feature.scala2macros || name == Feature.erasedDefinitions + + languageImport(qual) match + case Some(nme.experimental) + if !ctx.owner.isInExperimentalScope && !selectors.forall(isAllowedImport) => + def check(stable: => String) = + Feature.checkExperimentalFeature("features", imp.srcPos, + s"\n\nNote: the scope enclosing the import is not considered experimental because it contains the\nnon-experimental $stable") + if ctx.owner.is(Package) then + // allow top-level experimental imports if all definitions are @experimental + nonExperimentalStat(trees) match + case EmptyTree => + case tree: MemberDef => check(i"${tree.symbol}") + case tree => check(i"expression ${tree}") + else Feature.checkExperimentalFeature("features", imp.srcPos) + case _ => + end checkExperimentalImports } trait Checking { diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index b9e5198b0f24..815748d5a3ac 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -42,6 +42,7 @@ class CompilationTests { compileFilesInDir("tests/pos-custom-args/erased", defaultOptions.and("-language:experimental.erasedDefinitions")), compileFilesInDir("tests/pos", defaultOptions.and("-Ysafe-init")), compileFilesInDir("tests/pos-deep-subtype", allowDeepSubtypes), + compileFilesInDir("tests/pos-custom-args/no-experimental", defaultOptions.and("-Yno-experimental")), compileDir("tests/pos-special/java-param-names", defaultOptions.withJavacOnlyOptions("-parameters")), compileFile( // succeeds despite -Xfatal-warnings because of -nowarn @@ -178,7 +179,7 @@ class CompilationTests { compileFile("tests/neg-custom-args/matchable.scala", defaultOptions.and("-Xfatal-warnings", "-source", "future")), compileFile("tests/neg-custom-args/i7314.scala", defaultOptions.and("-Xfatal-warnings", "-source", "future")), compileFile("tests/neg-custom-args/feature-shadowing.scala", defaultOptions.and("-Xfatal-warnings", "-feature")), - compileDir("tests/neg-custom-args/hidden-type-errors", defaultOptions.and("-explain")), + compileDir("tests/neg-custom-args/hidden-type-errors", defaultOptions.and("-explain")), ).checkExpectedErrors() } diff --git a/docs/docs/reference/changed-features/numeric-literals.md b/docs/docs/reference/changed-features/numeric-literals.md index 3303043aa4ea..7705410ce369 100644 --- a/docs/docs/reference/changed-features/numeric-literals.md +++ b/docs/docs/reference/changed-features/numeric-literals.md @@ -4,259 +4,4 @@ title: "Numeric Literals" movedTo: https://docs.scala-lang.org/scala3/reference/changed-features/numeric-literals.html --- -**Note**: This feature is not yet part of the Scala 3 language definition. It can be made available by a language import: - -```scala -import scala.language.experimental.genericNumberLiterals -``` - -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: - -```scala -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 Trait - -To allow numeric literals, a type simply has to define a `given` instance of the -`scala.util.FromDigits` type class, 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: FromDigits.Floating[BigFloat] with - 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 metaprogramming. 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 FromDigits with - 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 is 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])(using ctx: Quotes): Expr[BigFloat] = - digits.value match - case Some(ds) => - try - val BigFloat(m, e) = apply(ds) - '{BigFloat(${Expr(m)}, ${Expr(e)})} - catch case ex: FromDigits.FromDigitsException => - ctx.error(ex.getMessage) - '{BigFloat(0, 0)} - case None => - '{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 is 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 -``` +[Document was moved](../experimental/numeric-literals.md) \ No newline at end of file diff --git a/docs/docs/reference/other-new-features/named-typeargs-spec.md b/docs/docs/reference/experimental/named-typeargs-spec.md similarity index 100% rename from docs/docs/reference/other-new-features/named-typeargs-spec.md rename to docs/docs/reference/experimental/named-typeargs-spec.md diff --git a/docs/docs/reference/experimental/named-typeargs.md b/docs/docs/reference/experimental/named-typeargs.md new file mode 100644 index 000000000000..e9a67b0368dd --- /dev/null +++ b/docs/docs/reference/experimental/named-typeargs.md @@ -0,0 +1,32 @@ +--- +layout: doc-page +title: "Named Type Arguments" +--- + +**Note:** This feature is implemented in Scala 3, but is not expected to be part of Scala 3.0. + +Type arguments of methods can now be specified by name as well as by position. Example: + +``` scala +def construct[Elem, Coll[_]](xs: Elem*): Coll[Elem] = ??? + +val xs1 = construct[Coll = List, Elem = Int](1, 2, 3) +val xs2 = construct[Coll = List](1, 2, 3) +``` + +Similar to a named value argument `(x = e)`, a named type argument +`[X = T]` instantiates the type parameter `X` to the type `T`. +Named type arguments do not have to be in order (see `xs1` above) and +unspecified arguments are inferred by the compiler (see `xs2` above). +Type arguments must be all named or un-named, mixtures of named and +positional type arguments are not supported. + +## Motivation + +The main benefit of named type arguments is that unlike positional arguments, +you are allowed to omit passing arguments for some parameters, like in the +definition of `xs2` above. A missing type argument is inferred as usual by +local type inference. This is particularly useful in situations where some type +arguments can be easily inferred from others. + +[More details](./named-typeargs-spec.md) diff --git a/docs/docs/reference/experimental/numeric-literals.md b/docs/docs/reference/experimental/numeric-literals.md new file mode 100644 index 000000000000..3303043aa4ea --- /dev/null +++ b/docs/docs/reference/experimental/numeric-literals.md @@ -0,0 +1,262 @@ +--- +layout: doc-page +title: "Numeric Literals" +movedTo: https://docs.scala-lang.org/scala3/reference/changed-features/numeric-literals.html +--- + +**Note**: This feature is not yet part of the Scala 3 language definition. It can be made available by a language import: + +```scala +import scala.language.experimental.genericNumberLiterals +``` + +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: + +```scala +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 Trait + +To allow numeric literals, a type simply has to define a `given` instance of the +`scala.util.FromDigits` type class, 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: FromDigits.Floating[BigFloat] with + 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 metaprogramming. 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 FromDigits with + 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 is 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])(using ctx: Quotes): Expr[BigFloat] = + digits.value match + case Some(ds) => + try + val BigFloat(m, e) = apply(ds) + '{BigFloat(${Expr(m)}, ${Expr(e)})} + catch case ex: FromDigits.FromDigitsException => + ctx.error(ex.getMessage) + '{BigFloat(0, 0)} + case None => + '{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 is 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/experimental/overview.md b/docs/docs/reference/experimental/overview.md new file mode 100644 index 000000000000..3a81c8e8bfe9 --- /dev/null +++ b/docs/docs/reference/experimental/overview.md @@ -0,0 +1,20 @@ +--- +layout: doc-page +title: Experimental language features +author: Nicolas Stucki +--- + +### Experimental language features + +All experimental language features can be found under the `scala.language.experimental` package. +They are enabled by importing the feature or using the `-language` compiler flag. + +* [`erasedDefinitions`](./erased-defs.md): Enable support for `erased` modifier. +* `fewerBraces`: Enable support for using indentation for arguments. +* [`genericNumberLiterals`](../numeric-literals.md): Enable support for generic number literals. +* [`namedTypeArguments`](../named-typeargs.md): Enable support for named type arguments + +### Experimental language imports + +In general, experimental language features can be imported in an experimental scope (see [experimental definitions](../other-new-features/experimental-defs.md). +They can be imported at the top-level if all top-level definitions are @experimental. diff --git a/docs/docs/reference/other-new-features/experimental-defs.md b/docs/docs/reference/other-new-features/experimental-defs.md index d3e42395162a..11121f1d0fcd 100644 --- a/docs/docs/reference/other-new-features/experimental-defs.md +++ b/docs/docs/reference/other-new-features/experimental-defs.md @@ -276,6 +276,7 @@ class A: class B extends A: - @experimental def f: Int = 2 ``` + ### Test frameworks Tests can be defined as experimental. Tests frameworks can execute tests using reflection even if they are in an experimental class, object or method. diff --git a/docs/docs/reference/other-new-features/named-typeargs.md b/docs/docs/reference/other-new-features/named-typeargs.md index e9a67b0368dd..11179c365da0 100644 --- a/docs/docs/reference/other-new-features/named-typeargs.md +++ b/docs/docs/reference/other-new-features/named-typeargs.md @@ -3,30 +3,4 @@ layout: doc-page title: "Named Type Arguments" --- -**Note:** This feature is implemented in Scala 3, but is not expected to be part of Scala 3.0. - -Type arguments of methods can now be specified by name as well as by position. Example: - -``` scala -def construct[Elem, Coll[_]](xs: Elem*): Coll[Elem] = ??? - -val xs1 = construct[Coll = List, Elem = Int](1, 2, 3) -val xs2 = construct[Coll = List](1, 2, 3) -``` - -Similar to a named value argument `(x = e)`, a named type argument -`[X = T]` instantiates the type parameter `X` to the type `T`. -Named type arguments do not have to be in order (see `xs1` above) and -unspecified arguments are inferred by the compiler (see `xs2` above). -Type arguments must be all named or un-named, mixtures of named and -positional type arguments are not supported. - -## Motivation - -The main benefit of named type arguments is that unlike positional arguments, -you are allowed to omit passing arguments for some parameters, like in the -definition of `xs2` above. A missing type argument is inferred as usual by -local type inference. This is particularly useful in situations where some type -arguments can be easily inferred from others. - -[More details](./named-typeargs-spec.md) +[Document was moved](../experimental/named-typeargs.md) diff --git a/docs/sidebar.yml b/docs/sidebar.yml index 4983c1e4b33a..783c97517ba7 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -107,6 +107,9 @@ sidebar: - page: docs/reference/dropped-features/nonlocal-returns.md - page: docs/reference/dropped-features/this-qualifier.md - page: docs/reference/dropped-features/wildcard-init.md + - title: Experimental Features + subsection: + - page: docs/reference/experimental/overview.md - page: docs/reference/syntax.md - title: Contributing subsection: diff --git a/tests/neg-custom-args/no-experimental/experimental-2.scala b/tests/neg-custom-args/no-experimental/experimental-2.scala new file mode 100644 index 000000000000..e2a8dcef58b8 --- /dev/null +++ b/tests/neg-custom-args/no-experimental/experimental-2.scala @@ -0,0 +1,5 @@ +class Test7 { + import scala.language.experimental + import experimental.genericNumberLiterals // error: no aliases can be used to refer to a language import + val x: BigInt = 13232202002020202020202 // error +} diff --git a/tests/neg-custom-args/no-experimental/experimental-imports.scala b/tests/neg-custom-args/no-experimental/experimental-imports.scala new file mode 100644 index 000000000000..63a150978b1c --- /dev/null +++ b/tests/neg-custom-args/no-experimental/experimental-imports.scala @@ -0,0 +1,46 @@ +import annotation.experimental + +@experimental +object Object1: + import language.experimental.fewerBraces + import language.experimental.namedTypeArguments + import language.experimental.genericNumberLiterals + import language.experimental.erasedDefinitions + erased def f = 1 + +object Object2: + import language.experimental.fewerBraces // error + import language.experimental.namedTypeArguments // error + import language.experimental.genericNumberLiterals // error + import language.experimental.erasedDefinitions + erased def f = 1 + +@experimental +object Class1: + import language.experimental.fewerBraces + import language.experimental.namedTypeArguments + import language.experimental.genericNumberLiterals + import language.experimental.erasedDefinitions + erased def f = 1 + +object Class2: + import language.experimental.fewerBraces // error + import language.experimental.namedTypeArguments // error + import language.experimental.genericNumberLiterals // error + import language.experimental.erasedDefinitions + erased def f = 1 + +@experimental +def fun1 = + import language.experimental.fewerBraces + import language.experimental.namedTypeArguments + import language.experimental.genericNumberLiterals + import language.experimental.erasedDefinitions + erased def f = 1 + +def fun2 = + import language.experimental.fewerBraces // error + import language.experimental.namedTypeArguments // error + import language.experimental.genericNumberLiterals // error + import language.experimental.erasedDefinitions + erased def f = 1 diff --git a/tests/neg-custom-args/no-experimental/experimental-nested-imports-2.scala b/tests/neg-custom-args/no-experimental/experimental-nested-imports-2.scala new file mode 100644 index 000000000000..85076cca723a --- /dev/null +++ b/tests/neg-custom-args/no-experimental/experimental-nested-imports-2.scala @@ -0,0 +1,33 @@ +import annotation.experimental + +class Class1: + import language.experimental.fewerBraces // error + import language.experimental.namedTypeArguments // error + import language.experimental.genericNumberLiterals // error + import language.experimental.erasedDefinitions // ok: only check at erased definition + @experimental def f = 1 + def g = 1 + +object Object1: + import language.experimental.fewerBraces // error + import language.experimental.namedTypeArguments // error + import language.experimental.genericNumberLiterals // error + import language.experimental.erasedDefinitions // ok: only check at erased definition + @experimental def f = 1 + def g = 1 + +def fun1 = + import language.experimental.fewerBraces // error + import language.experimental.namedTypeArguments // error + import language.experimental.genericNumberLiterals // error + import language.experimental.erasedDefinitions // ok: only check at erased definition + @experimental def f = 1 + def g = 1 + +val value1 = + import language.experimental.fewerBraces // error + import language.experimental.namedTypeArguments // error + import language.experimental.genericNumberLiterals // error + import language.experimental.erasedDefinitions // ok: only check at erased definition + @experimental def f = 1 + def g = 1 \ No newline at end of file diff --git a/tests/neg-custom-args/no-experimental/experimental-nested-imports-3.scala b/tests/neg-custom-args/no-experimental/experimental-nested-imports-3.scala new file mode 100644 index 000000000000..1af04918b1d9 --- /dev/null +++ b/tests/neg-custom-args/no-experimental/experimental-nested-imports-3.scala @@ -0,0 +1,25 @@ +import annotation.experimental + +class Class1: + import language.experimental.fewerBraces // error + import language.experimental.namedTypeArguments // error + import language.experimental.genericNumberLiterals // error + import language.experimental.erasedDefinitions // ok: only check at erased definition + +object Object1: + import language.experimental.fewerBraces // error + import language.experimental.namedTypeArguments // error + import language.experimental.genericNumberLiterals // error + import language.experimental.erasedDefinitions // ok: only check at erased definition + +def fun1 = + import language.experimental.fewerBraces // error + import language.experimental.namedTypeArguments // error + import language.experimental.genericNumberLiterals // error + import language.experimental.erasedDefinitions // ok: only check at erased definition + +val value1 = + import language.experimental.fewerBraces // error + import language.experimental.namedTypeArguments // error + import language.experimental.genericNumberLiterals // error + import language.experimental.erasedDefinitions // ok: only check at erased definition diff --git a/tests/neg-custom-args/no-experimental/experimental-nested-imports.scala b/tests/neg-custom-args/no-experimental/experimental-nested-imports.scala new file mode 100644 index 000000000000..b9fc38dc4915 --- /dev/null +++ b/tests/neg-custom-args/no-experimental/experimental-nested-imports.scala @@ -0,0 +1,29 @@ +import annotation.experimental + +class Class1: + import language.experimental.fewerBraces // error + import language.experimental.namedTypeArguments // error + import language.experimental.genericNumberLiterals // error + import language.experimental.erasedDefinitions // ok: only check at erased definition + @experimental def f = 1 + +object Object1: + import language.experimental.fewerBraces // error + import language.experimental.namedTypeArguments // error + import language.experimental.genericNumberLiterals // error + import language.experimental.erasedDefinitions // ok: only check at erased definition + @experimental def f = 1 + +def fun1 = + import language.experimental.fewerBraces // error + import language.experimental.namedTypeArguments // error + import language.experimental.genericNumberLiterals // error + import language.experimental.erasedDefinitions // ok: only check at erased definition + @experimental def f = 1 + +val value1 = + import language.experimental.fewerBraces // error + import language.experimental.namedTypeArguments // error + import language.experimental.genericNumberLiterals // error + import language.experimental.erasedDefinitions // ok: only check at erased definition + @experimental def f = 1 diff --git a/tests/neg-custom-args/no-experimental/experimental-package-imports.scala b/tests/neg-custom-args/no-experimental/experimental-package-imports.scala new file mode 100644 index 000000000000..90ec387b1036 --- /dev/null +++ b/tests/neg-custom-args/no-experimental/experimental-package-imports.scala @@ -0,0 +1,24 @@ +import annotation.experimental + +package foo { + import language.experimental.fewerBraces // error + import language.experimental.namedTypeArguments // error + import language.experimental.genericNumberLiterals // error + import language.experimental.erasedDefinitions // ok: only check at erased definition + + package bar { + def foo = 1 + } +} + +package foo2 { + // ok: all definitions are top-level @experimental + import language.experimental.fewerBraces + import language.experimental.namedTypeArguments + import language.experimental.genericNumberLiterals + import language.experimental.erasedDefinitions + + package bar { + @experimental def foo = 1 + } +} diff --git a/tests/neg-custom-args/no-experimental/experimental.scala b/tests/neg-custom-args/no-experimental/experimental.scala index c1fb43db8455..42d0d8066c35 100644 --- a/tests/neg-custom-args/no-experimental/experimental.scala +++ b/tests/neg-custom-args/no-experimental/experimental.scala @@ -26,9 +26,3 @@ class Test2 { class Test6 { import scala.language.experimental // ok } - -class Test7 { - import scala.language.experimental - import experimental.genericNumberLiterals // error: no aliases can be used to refer to a language import - val x: BigInt = 13232202002020202020202 // error -} \ No newline at end of file diff --git a/tests/pos-custom-args/no-experimental/experimental-imports-empty.scala b/tests/pos-custom-args/no-experimental/experimental-imports-empty.scala new file mode 100644 index 000000000000..bb27629a6062 --- /dev/null +++ b/tests/pos-custom-args/no-experimental/experimental-imports-empty.scala @@ -0,0 +1,5 @@ +import annotation.experimental +import language.experimental.fewerBraces +import language.experimental.namedTypeArguments +import language.experimental.genericNumberLiterals +import language.experimental.erasedDefinitions diff --git a/tests/pos-custom-args/no-experimental/experimental-imports-top.scala b/tests/pos-custom-args/no-experimental/experimental-imports-top.scala new file mode 100644 index 000000000000..bee89d6ab6c8 --- /dev/null +++ b/tests/pos-custom-args/no-experimental/experimental-imports-top.scala @@ -0,0 +1,5 @@ +import language.experimental.erasedDefinitions +import annotation.experimental + +@experimental +erased def f = 1