diff --git a/docs/docs/reference/metaprogramming/compiletime-ops.md b/docs/docs/reference/metaprogramming/compiletime-ops.md new file mode 100644 index 000000000000..86d146818dd2 --- /dev/null +++ b/docs/docs/reference/metaprogramming/compiletime-ops.md @@ -0,0 +1,274 @@ +--- +layout: doc-page +title: "Compile-time operations" +--- + +## The `scala.compiletime` Package + +The [`scala.compiletime`](https://dotty.epfl.ch/api/scala/compiletime.html) package contains helper definitions that provide support for compile-time operations over values. They are described in the following. + +### `constValue` and `constValueOpt` + +`constValue` is a function that produces the constant value represented by a +type. + +```scala +import scala.compiletime.constValue +import scala.compiletime.ops.int.S + +transparent inline def toIntC[N]: Int = + inline constValue[N] match + case 0 => 0 + case _: S[n1] => 1 + toIntC[n1] + +inline val ctwo = toIntC[2] +``` + +`constValueOpt` is the same as `constValue`, however returning an `Option[T]` +enabling us to handle situations where a value is not present. Note that `S` is +the type of the successor of some singleton type. For example the type `S[1]` is +the singleton type `2`. + +### `erasedValue` + +So far we have seen inline methods that take terms (tuples and integers) as +parameters. What if we want to base case distinctions on types instead? For +instance, one would like to be able to write a function `defaultValue`, that, +given a type `T`, returns optionally the default value of `T`, if it exists. +We can already express this using rewrite match expressions and a simple +helper function, `scala.compiletime.erasedValue`, which is defined as follows: + +```scala +def erasedValue[T]: T +``` + +The `erasedValue` function _pretends_ to return a value of its type argument `T`. +Calling this function will always result in a compile-time error unless the call +is removed from the code while inlining. + +Using `erasedValue`, we can then define `defaultValue` as follows: + +```scala +import scala.compiletime.erasedValue + +inline def defaultValue[T] = + inline erasedValue[T] match + case _: Byte => Some(0: Byte) + case _: Char => Some(0: Char) + case _: Short => Some(0: Short) + case _: Int => Some(0) + case _: Long => Some(0L) + case _: Float => Some(0.0f) + case _: Double => Some(0.0d) + case _: Boolean => Some(false) + case _: Unit => Some(()) + case _ => None +``` + +Then: + +```scala +val dInt: Some[Int] = defaultValue[Int] +val dDouble: Some[Double] = defaultValue[Double] +val dBoolean: Some[Boolean] = defaultValue[Boolean] +val dAny: None.type = defaultValue[Any] +``` + +As another example, consider the type-level version of `toInt` below: +given a _type_ representing a Peano number, +return the integer _value_ corresponding to it. +Consider the definitions of numbers as in the _Inline +Match_ section above. Here is how `toIntT` can be defined: + +```scala +transparent inline def toIntT[N <: Nat]: Int = + inline scala.compiletime.erasedValue[N] match + case _: Zero.type => 0 + case _: Succ[n] => toIntT[n] + 1 + +inline val two = toIntT[Succ[Succ[Zero.type]]] +``` + +`erasedValue` is an `erased` method so it cannot be used and has no runtime +behavior. Since `toIntT` performs static checks over the static type of `N` we +can safely use it to scrutinize its return type (`S[S[Z]]` in this case). + +### `error` + +The `error` method is used to produce user-defined compile errors during inline expansion. +It has the following signature: + +```scala +inline def error(inline msg: String): Nothing +``` + +If an inline expansion results in a call `error(msgStr)` the compiler +produces an error message containing the given `msgStr`. + +```scala +import scala.compiletime.{error, code} + +inline def fail() = + error("failed for a reason") + +fail() // error: failed for a reason +``` + +or + +```scala +inline def fail(p1: => Any) = + error(code"failed on: $p1") + +fail(identity("foo")) // error: failed on: identity("foo") +``` + +### The `scala.compiletime.ops` package + +The [`scala.compiletime.ops`](https://dotty.epfl.ch/api/scala/compiletime/ops.html) package contains types that provide support for +primitive operations on singleton types. For example, +`scala.compiletime.ops.int.*` provides support for multiplying two singleton +`Int` types, and `scala.compiletime.ops.boolean.&&` for the conjunction of two +`Boolean` types. When all arguments to a type in `scala.compiletime.ops` are +singleton types, the compiler can evaluate the result of the operation. + +```scala +import scala.compiletime.ops.int.* +import scala.compiletime.ops.boolean.* + +val conjunction: true && true = true +val multiplication: 3 * 5 = 15 +``` + +Many of these singleton operation types are meant to be used infix (as in [SLS §3.2.10](https://www.scala-lang.org/files/archive/spec/2.13/03-types.html#infix-types)). + +Since type aliases have the same precedence rules as their term-level +equivalents, the operations compose with the expected precedence rules: + +```scala +import scala.compiletime.ops.int.* +val x: 1 + 2 * 3 = 7 +``` + +The operation types are located in packages named after the type of the +left-hand side parameter: for instance, `scala.compiletime.ops.int.+` represents +addition of two numbers, while `scala.compiletime.ops.string.+` represents string +concatenation. To use both and distinguish the two types from each other, a +match type can dispatch to the correct implementation: + +```scala +import scala.compiletime.ops.* + +import scala.annotation.infix + +type +[X <: Int | String, Y <: Int | String] = (X, Y) match + case (Int, Int) => int.+[X, Y] + case (String, String) => string.+[X, Y] + +val concat: "a" + "b" = "ab" +val addition: 1 + 1 = 2 +``` + +## Summoning Implicits Selectively + +It is foreseen that many areas of typelevel programming can be done with rewrite +methods instead of implicits. But sometimes implicits are unavoidable. The +problem so far was that the Prolog-like programming style of implicit search +becomes viral: Once some construct depends on implicit search it has to be +written as a logic program itself. Consider for instance the problem of creating +a `TreeSet[T]` or a `HashSet[T]` depending on whether `T` has an `Ordering` or +not. We can create a set of implicit definitions like this: + +```scala +trait SetFor[T, S <: Set[T]] + +class LowPriority: + implicit def hashSetFor[T]: SetFor[T, HashSet[T]] = ... + +object SetsFor extends LowPriority: + implicit def treeSetFor[T: Ordering]: SetFor[T, TreeSet[T]] = ... +``` + +Clearly, this is not pretty. Besides all the usual indirection of implicit +search, we face the problem of rule prioritization where we have to ensure that +`treeSetFor` takes priority over `hashSetFor` if the element type has an +ordering. This is solved (clumsily) by putting `hashSetFor` in a superclass +`LowPriority` of the object `SetsFor` where `treeSetFor` is defined. Maybe the +boilerplate would still be acceptable if the crufty code could be contained. +However, this is not the case. Every user of the abstraction has to be +parameterized itself with a `SetFor` implicit. Considering the simple task _"I +want a `TreeSet[T]` if `T` has an ordering and a `HashSet[T]` otherwise"_, this +seems like a lot of ceremony. + +There are some proposals to improve the situation in specific areas, for +instance by allowing more elaborate schemes to specify priorities. But they all +keep the viral nature of implicit search programs based on logic programming. + +By contrast, the new `summonFrom` construct makes implicit search available +in a functional context. To solve the problem of creating the right set, one +would use it as follows: + +```scala +import scala.compiletime.summonFrom + +inline def setFor[T]: Set[T] = summonFrom { + case ord: Ordering[T] => new TreeSet[T](using ord) + case _ => new HashSet[T] +} +``` + +A `summonFrom` call takes a pattern matching closure as argument. All patterns +in the closure are type ascriptions of the form `identifier : Type`. + +Patterns are tried in sequence. The first case with a pattern `x: T` such that an implicit value of type `T` can be summoned is chosen. + +Alternatively, one can also use a pattern-bound given instance, which avoids the explicit using clause. For instance, `setFor` could also be formulated as follows: + +```scala +import scala.compiletime.summonFrom + +inline def setFor[T]: Set[T] = summonFrom { + case given Ordering[T] => new TreeSet[T] + case _ => new HashSet[T] +} +``` + +`summonFrom` applications must be reduced at compile time. + +Consequently, if we summon an `Ordering[String]` the code above will return a +new instance of `TreeSet[String]`. + +```scala +summon[Ordering[String]] + +println(setFor[String].getClass) // prints class scala.collection.immutable.TreeSet +``` + +**Note** `summonFrom` applications can raise ambiguity errors. Consider the following +code with two givens in scope of type `A`. The pattern match in `f` will raise +an ambiguity error of `f` is applied. + +```scala +class A +given a1: A = new A +given a2: A = new A + +inline def f: Any = summonFrom { + case given _: A => ??? // error: ambiguous givens +} +``` + +## `summonInline` + +The shorthand `summonInline` provides a simple way to write a `summon` that is delayed until the call is inlined. + +```scala +transparent inline def summonInline[T]: T = summonFrom { + case t: T => t +} +``` + +## Reference + +For more information about compile-time operations, see [PR #4768](https://github.com/lampepfl/dotty/pull/4768), +which explains how `summonFrom`'s predecessor (implicit matches) can be used for typelevel programming and code specialization and [PR #7201](https://github.com/lampepfl/dotty/pull/7201) which explains the new `summonFrom` syntax. diff --git a/docs/docs/reference/metaprogramming/inline.md b/docs/docs/reference/metaprogramming/inline.md index d5b1ce680bb9..ee8ebe9d01d7 100644 --- a/docs/docs/reference/metaprogramming/inline.md +++ b/docs/docs/reference/metaprogramming/inline.md @@ -372,274 +372,6 @@ val intTwo: 2 = natTwo `natTwo` is inferred to have the singleton type 2. -## The `scala.compiletime` Package - -The [`scala.compiletime`](https://dotty.epfl.ch/api/scala/compiletime.html) package contains helper definitions that provide support for compile time operations over values. They are described in the following. - -### `constValue` and `constValueOpt` - -`constValue` is a function that produces the constant value represented by a -type. - -```scala -import scala.compiletime.constValue -import scala.compiletime.ops.int.S - -transparent inline def toIntC[N]: Int = - inline constValue[N] match - case 0 => 0 - case _: S[n1] => 1 + toIntC[n1] - -inline val ctwo = toIntC[2] -``` - -`constValueOpt` is the same as `constValue`, however returning an `Option[T]` -enabling us to handle situations where a value is not present. Note that `S` is -the type of the successor of some singleton type. For example the type `S[1]` is -the singleton type `2`. - -### `erasedValue` - -So far we have seen inline methods that take terms (tuples and integers) as -parameters. What if we want to base case distinctions on types instead? For -instance, one would like to be able to write a function `defaultValue`, that, -given a type `T`, returns optionally the default value of `T`, if it exists. -We can already express this using rewrite match expressions and a simple -helper function, `scala.compiletime.erasedValue`, which is defined as follows: - -```scala -def erasedValue[T]: T -``` - -The `erasedValue` function _pretends_ to return a value of its type argument `T`. -Calling this function will always result in a compiletime error unless the call -is removed from the code while inlining. - -Using `erasedValue`, we can then define `defaultValue` as follows: - -```scala -import scala.compiletime.erasedValue - -inline def defaultValue[T] = - inline erasedValue[T] match - case _: Byte => Some(0: Byte) - case _: Char => Some(0: Char) - case _: Short => Some(0: Short) - case _: Int => Some(0) - case _: Long => Some(0L) - case _: Float => Some(0.0f) - case _: Double => Some(0.0d) - case _: Boolean => Some(false) - case _: Unit => Some(()) - case _ => None -``` - -Then: - -```scala -val dInt: Some[Int] = defaultValue[Int] -val dDouble: Some[Double] = defaultValue[Double] -val dBoolean: Some[Boolean] = defaultValue[Boolean] -val dAny: None.type = defaultValue[Any] -``` - -As another example, consider the type-level version of `toInt` below: -given a _type_ representing a Peano number, -return the integer _value_ corresponding to it. -Consider the definitions of numbers as in the _Inline -Match_ section above. Here is how `toIntT` can be defined: - -```scala -transparent inline def toIntT[N <: Nat]: Int = - inline scala.compiletime.erasedValue[N] match - case _: Zero.type => 0 - case _: Succ[n] => toIntT[n] + 1 - -inline val two = toIntT[Succ[Succ[Zero.type]]] -``` - -`erasedValue` is an `erased` method so it cannot be used and has no runtime -behavior. Since `toIntT` performs static checks over the static type of `N` we -can safely use it to scrutinize its return type (`S[S[Z]]` in this case). - -### `error` - -The `error` method is used to produce user-defined compile errors during inline expansion. -It has the following signature: - -```scala -inline def error(inline msg: String): Nothing -``` - -If an inline expansion results in a call `error(msgStr)` the compiler -produces an error message containing the given `msgStr`. - -```scala -import scala.compiletime.{error, code} - -inline def fail() = - error("failed for a reason") - -fail() // error: failed for a reason -``` - -or - -```scala -inline def fail(p1: => Any) = - error(code"failed on: $p1") - -fail(identity("foo")) // error: failed on: identity("foo") -``` - -### The `scala.compiletime.ops` package - -The [`scala.compiletime.ops`](https://dotty.epfl.ch/api/scala/compiletime/ops.html) package contains types that provide support for -primitive operations on singleton types. For example, -`scala.compiletime.ops.int.*` provides support for multiplying two singleton -`Int` types, and `scala.compiletime.ops.boolean.&&` for the conjunction of two -`Boolean` types. When all arguments to a type in `scala.compiletime.ops` are -singleton types, the compiler can evaluate the result of the operation. - -```scala -import scala.compiletime.ops.int.* -import scala.compiletime.ops.boolean.* - -val conjunction: true && true = true -val multiplication: 3 * 5 = 15 -``` - -Many of these singleton operation types are meant to be used infix (as in [SLS §3.2.10](https://www.scala-lang.org/files/archive/spec/2.13/03-types.html#infix-types)). - -Since type aliases have the same precedence rules as their term-level -equivalents, the operations compose with the expected precedence rules: - -```scala -import scala.compiletime.ops.int.* -val x: 1 + 2 * 3 = 7 -``` - -The operation types are located in packages named after the type of the -left-hand side parameter: for instance, `scala.compiletime.ops.int.+` represents -addition of two numbers, while `scala.compiletime.ops.string.+` represents string -concatenation. To use both and distinguish the two types from each other, a -match type can dispatch to the correct implementation: - -```scala -import scala.compiletime.ops.* - -import scala.annotation.infix - -type +[X <: Int | String, Y <: Int | String] = (X, Y) match - case (Int, Int) => int.+[X, Y] - case (String, String) => string.+[X, Y] - -val concat: "a" + "b" = "ab" -val addition: 1 + 1 = 2 -``` - -## Summoning Implicits Selectively - -It is foreseen that many areas of typelevel programming can be done with rewrite -methods instead of implicits. But sometimes implicits are unavoidable. The -problem so far was that the Prolog-like programming style of implicit search -becomes viral: Once some construct depends on implicit search it has to be -written as a logic program itself. Consider for instance the problem of creating -a `TreeSet[T]` or a `HashSet[T]` depending on whether `T` has an `Ordering` or -not. We can create a set of implicit definitions like this: - -```scala -trait SetFor[T, S <: Set[T]] - -class LowPriority: - implicit def hashSetFor[T]: SetFor[T, HashSet[T]] = ... - -object SetsFor extends LowPriority: - implicit def treeSetFor[T: Ordering]: SetFor[T, TreeSet[T]] = ... -``` - -Clearly, this is not pretty. Besides all the usual indirection of implicit -search, we face the problem of rule prioritization where we have to ensure that -`treeSetFor` takes priority over `hashSetFor` if the element type has an -ordering. This is solved (clumsily) by putting `hashSetFor` in a superclass -`LowPriority` of the object `SetsFor` where `treeSetFor` is defined. Maybe the -boilerplate would still be acceptable if the crufty code could be contained. -However, this is not the case. Every user of the abstraction has to be -parameterized itself with a `SetFor` implicit. Considering the simple task _"I -want a `TreeSet[T]` if `T` has an ordering and a `HashSet[T]` otherwise"_, this -seems like a lot of ceremony. - -There are some proposals to improve the situation in specific areas, for -instance by allowing more elaborate schemes to specify priorities. But they all -keep the viral nature of implicit search programs based on logic programming. - -By contrast, the new `summonFrom` construct makes implicit search available -in a functional context. To solve the problem of creating the right set, one -would use it as follows: - -```scala -import scala.compiletime.summonFrom - -inline def setFor[T]: Set[T] = summonFrom { - case ord: Ordering[T] => new TreeSet[T](using ord) - case _ => new HashSet[T] -} -``` - -A `summonFrom` call takes a pattern matching closure as argument. All patterns -in the closure are type ascriptions of the form `identifier : Type`. - -Patterns are tried in sequence. The first case with a pattern `x: T` such that an implicit value of type `T` can be summoned is chosen. - -Alternatively, one can also use a pattern-bound given instance, which avoids the explicit using clause. For instance, `setFor` could also be formulated as follows: - -```scala -import scala.compiletime.summonFrom - -inline def setFor[T]: Set[T] = summonFrom { - case given Ordering[T] => new TreeSet[T] - case _ => new HashSet[T] -} -``` - -`summonFrom` applications must be reduced at compile time. - -Consequently, if we summon an `Ordering[String]` the code above will return a -new instance of `TreeSet[String]`. - -```scala -summon[Ordering[String]] - -println(setFor[String].getClass) // prints class scala.collection.immutable.TreeSet -``` - -**Note** `summonFrom` applications can raise ambiguity errors. Consider the following -code with two givens in scope of type `A`. The pattern match in `f` will raise -an ambiguity error of `f` is applied. - -```scala -class A -given a1: A = new A -given a2: A = new A - -inline def f: Any = summonFrom { - case given _: A => ??? // error: ambiguous givens -} -``` - -## `summonInline` - -The shorthand `summonInline` provides a simple way to write a `summon` that is delayed until the call is inlined. - -```scala -transparent inline def summonInline[T]: T = summonFrom { - case t: T => t -} -``` - ### Reference For more information about the semantics of `inline`, see the [Scala 2020: Semantics-preserving inlining for metaprogramming](https://dl.acm.org/doi/10.1145/3426426.3428486) paper. - -For more information about compiletime operation, see [PR #4768](https://github.com/lampepfl/dotty/pull/4768), -which explains how `summonFrom`'s predecessor (implicit matches) can be used for typelevel programming and code specialization and [PR #7201](https://github.com/lampepfl/dotty/pull/7201) which explains the new `summonFrom` syntax. diff --git a/docs/docs/reference/metaprogramming/toc.md b/docs/docs/reference/metaprogramming/toc.md index 9d1e0b2a0405..4baa12e64e55 100644 --- a/docs/docs/reference/metaprogramming/toc.md +++ b/docs/docs/reference/metaprogramming/toc.md @@ -17,7 +17,10 @@ introduce the following fundamental facilities: programming), macros (enabling compile-time, generative, metaprogramming) and runtime code generation (multi-stage programming). -2. [Macros](./macros.md) are built on two well-known fundamental +2. [Compile-time ops](./compiletime-ops.md) are helper definitions in the + standard library that provide support for compile-time operations over values and types. + +3. [Macros](./macros.md) are built on two well-known fundamental operations: quotation and splicing. Quotation converts program code to data, specifically, a (tree-like) representation of this code. It is expressed as `'{...}` for expressions and as `'[...]` for types. Splicing, @@ -25,20 +28,19 @@ introduce the following fundamental facilities: to program code. Together with `inline`, these two abstractions allow to construct program code programmatically. -3. [Runtime Staging](./staging.md) Where macros construct code at _compile-time_, +4. [Runtime Staging](./staging.md) Where macros construct code at _compile-time_, staging lets programs construct new code at _runtime_. That way, code generation can depend not only on static data but also on data available at runtime. This splits the evaluation of the program in two or more phases or ... stages. Consequently, this method of generative programming is called "Multi-Stage Programming". Staging is built on the same foundations as macros. It uses quotes and splices, but leaves out `inline`. -4. [Reflection](./reflection.md) Quotations are a "black-box" +5. [Reflection](./reflection.md) Quotations are a "black-box" representation of code. They can be parameterized and composed using splices, but their structure cannot be analyzed from the outside. TASTy reflection gives a way to analyze code structure by partly revealing the representation type of a piece of code in a standard API. The representation type is a form of typed abstract syntax tree, which gives rise to the `TASTy` moniker. -5. [TASTy Inspection](./tasty-inspect.md) Typed abstract syntax trees are serialized +6. [TASTy Inspection](./tasty-inspect.md) Typed abstract syntax trees are serialized in a custom compressed binary format stored in `.tasty` files. TASTy inspection allows to load these files and analyze their content's tree structure. - diff --git a/docs/sidebar.yml b/docs/sidebar.yml index 4e87972ed9b6..980ecca16edb 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -79,6 +79,8 @@ sidebar: url: docs/reference/metaprogramming/toc.html - title: Inline url: docs/reference/metaprogramming/inline.html + - title: Compile-time ops + url: docs/reference/metaprogramming/compiletime-ops.html - title: Macros url: docs/reference/metaprogramming/macros.html - title: Runtime Staging