Skip to content

Commit 9982f0d

Browse files
Merge pull request #8061 from dotty-staging/disallow-phase-inconsistent-inline-params
Disallow phase inconsistent inline parameters
2 parents 8dfa9a5 + e56574f commit 9982f0d

File tree

66 files changed

+611
-208
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+611
-208
lines changed

compiler/src/dotty/tools/dotc/transform/PCPCheckAndHeal.scala

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,6 @@ class PCPCheckAndHeal(@constructorOnly ictx: Context) extends TreeMapWithStages(
8888
tree match {
8989
case Quoted(_) | Spliced(_) =>
9090
tree
91-
case tree: RefTree if tree.symbol.isAllOf(InlineParam) =>
92-
tree
9391
case _: This =>
9492
assert(checkSymLevel(tree.symbol, tree.tpe, tree.sourcePos).isEmpty)
9593
tree
@@ -197,10 +195,8 @@ class PCPCheckAndHeal(@constructorOnly ictx: Context) extends TreeMapWithStages(
197195
case Some(l) =>
198196
l == level ||
199197
level == -1 && (
200-
// here we assume that Splicer.canBeSpliced was true before going to level -1,
201-
// this implies that all non-inline arguments are quoted and that the following two cases are checked
202-
// on inline parameters or type parameters.
203-
sym.is(Param) ||
198+
// here we assume that Splicer.checkValidMacroBody was true before going to level -1,
199+
// this implies that all arguments are quoted.
204200
sym.isClass // reference to this in inline methods
205201
)
206202
case None =>

compiler/src/dotty/tools/dotc/transform/Splicer.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ object Splicer {
5252
catch {
5353
case ex: CompilationUnit.SuspendException =>
5454
throw ex
55+
case ex: scala.quoted.StopQuotedContext if ctx.reporter.hasErrors =>
56+
// errors have been emitted
57+
EmptyTree
5558
case ex: StopInterpretation =>
5659
ctx.error(ex.msg, ex.pos)
5760
EmptyTree
@@ -389,6 +392,8 @@ object Splicer {
389392
throw new StopInterpretation(sw.toString, pos)
390393
case ex: InvocationTargetException =>
391394
ex.getTargetException match {
395+
case ex: scala.quoted.StopQuotedContext =>
396+
throw ex
392397
case MissingClassDefinedInCurrentRun(sym) =>
393398
if (ctx.settings.XprintSuspension.value)
394399
ctx.echo(i"suspension triggered by a dependency on $sym", pos)

compiler/src/dotty/tools/dotc/transform/TreeMapWithStages.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import dotty.tools.dotc.util.Spans._
1919
import dotty.tools.dotc.util.{Property, SourcePosition}
2020
import dotty.tools.dotc.transform.SymUtils._
2121
import dotty.tools.dotc.typer.Implicits.SearchFailureType
22-
import dotty.tools.dotc.typer.Inliner
2322

2423
import scala.collection.mutable
2524
import scala.annotation.constructorOnly

docs/docs/reference/metaprogramming/macros.md

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -356,11 +356,11 @@ again together with a program that calls `assert`.
356356
```scala
357357
object Macros {
358358

359-
inline def assert(expr: => Boolean): Unit =
359+
inline def assert(inline expr: Boolean): Unit =
360360
${ assertImpl('expr) }
361361

362362
def assertImpl(expr: Expr[Boolean]) =
363-
'{ if !($expr) then throw new AssertionError(s"failed assertion: ${$expr}") }
363+
'{ if !($expr) then throw new AssertionError("failed assertion: " + ${expr.show}) }
364364
}
365365

366366
object App {
@@ -414,33 +414,28 @@ assume that both definitions are local.
414414

415415
The `inline` modifier is used to declare a `val` that is
416416
either a constant or is a parameter that will be a constant when instantiated. This
417-
aspect is also important for macro expansion. To illustrate this,
418-
consider an implementation of the `power` function that makes use of a
419-
statically known exponent:
417+
aspect is also important for macro expansion.
418+
419+
To get values out of expressions containing constants `Expr` provides the method
420+
`getValue` (or `value`). This will convert the `Expr[T]` into a `Some[T]` (or `T`) when the
421+
expression contains value. Otherwise it will retrun `None` (or emit an error).
422+
To avoid having incidental val bindings generated by the inlining of the `def`
423+
it is recommended to use an inline parameter. To illustrate this, consider an
424+
implementation of the `power` function that makes use of a statically known exponent:
420425
```scala
421-
inline def power(inline n: Int, x: Double) = ${ powerCode(n, 'x) }
426+
inline def power(x: Double, inline n: Int) = ${ powerCode('x, 'n) }
427+
428+
private def powerCode(x: Expr[Double], n: Expr[Int])(given QuoteContext): Expr[Double] =
429+
n.getValue match
430+
case Some(m) => powerCode(x, m)
431+
case None => '{ Math.pow($x, $y) }
422432

423-
private def powerCode(n: Int, x: Expr[Double]): Expr[Double] =
433+
private def powerCode(x: Expr[Double], n: Int)(given QuoteContext): Expr[Double] =
424434
if (n == 0) '{ 1.0 }
425435
else if (n == 1) x
426-
else if (n % 2 == 0) '{ val y = $x * $x; ${ powerCode(n / 2, 'y) } }
427-
else '{ $x * ${ powerCode(n - 1, x) } }
428-
```
429-
The reference to `n` as an argument in `${ powerCode(n, 'x) }` is not
430-
phase-consistent, since `n` appears in a splice without an enclosing
431-
quote. Normally that would be a problem because it means that we need
432-
the _value_ of `n` at compile time, which is not available for general
433-
parameters. But since `n` is an inline parameter of a macro, we know
434-
that at the macro’s expansion point `n` will be instantiated to a
435-
constant, so the value of `n` will in fact be known at this
436-
point. To reflect this, we loosen the phase consistency requirements
437-
as follows:
438-
439-
- If `x` is a inline value (or a inline parameter of an inline
440-
function) of type Boolean, Byte, Short, Int, Long, Float, Double,
441-
Char or String, it can be accessed in all contexts where the number
442-
of splices minus the number of quotes between use and definition
443-
is either 0 or 1.
436+
else if (n % 2 == 0) '{ val y = $x * $x; ${ powerCode('y, n / 2) } }
437+
else '{ $x * ${ powerCode(x, n - 1) } }
438+
```
444439

445440
### Scope Extrusion
446441

@@ -472,7 +467,7 @@ that invokation of `run` in splices. Consider the following expression:
472467
'{ (x: Int) => ${ run('x); 1 } }
473468
```
474469
This is again phase correct, but will lead us into trouble. Indeed, evaluating
475-
the splice will reduce the expression `('x).run` to `x`. But then the result
470+
the splice will reduce the expression `run('x)` to `x`. But then the result
476471

477472
```scala
478473
'{ (x: Int) => ${ x; 1 } }
@@ -590,12 +585,12 @@ inline method that can calculate either a value of type `Int` or a value of type
590585
`String`.
591586

592587
```scala
593-
inline def defaultOf(inline str: String) <: Any = ${ defaultOfImpl(str) }
588+
inline def defaultOf(inline str: String) <: Any = ${ defaultOfImpl('str) }
594589

595-
def defaultOfImpl(str: String): Expr[Any] = str match {
596-
case "int" => '{1}
597-
case "string" => '{"a"}
598-
}
590+
def defaultOfImpl(strExpr: Expr[String])(given QuoteContext): Expr[Any] =
591+
strExpr.value match
592+
case "int" => '{1}
593+
case "string" => '{"a"}
599594

600595
// in a separate file
601596
val a: Int = defaultOf("int")
@@ -624,8 +619,10 @@ It is possible to deconstruct or extract values out of `Expr` using pattern matc
624619
In `scala.quoted.matching` contains object that can help extract values from `Expr`.
625620

626621
* `scala.quoted.matching.Const`: matches an expression a literal value and returns the value.
622+
* `scala.quoted.matching.Value`: matches an expression a value and returns the value.
627623
* `scala.quoted.matching.ExprSeq`: matches an explicit sequence of expresions and returns them. These sequences are useful to get individual `Expr[T]` out of a varargs expression of type `Expr[Seq[T]]`.
628624
* `scala.quoted.matching.ConstSeq`: matches an explicit sequence of literal values and returns them.
625+
* `scala.quoted.matching.ValueSeq`: matches an explicit sequence of values and returns them.
629626

630627
These could be used in the following way to optimize any call to `sum` that has statically known values.
631628
```scala
@@ -661,7 +658,7 @@ optimize {
661658
```
662659

663660
```scala
664-
def sum(args: =>Int*): Int = args.sum
661+
def sum(args: Int*): Int = args.sum
665662
inline def optimize(arg: Int): Int = ${ optimizeExpr('arg) }
666663
private def optimizeExpr(body: Expr[Int])(given QuoteContext): Expr[Int] = body match {
667664
// Match a call to sum without any arguments
@@ -695,7 +692,7 @@ private def sumExpr(args1: Seq[Expr[Int]])(given QuoteContext): Expr[Int] = {
695692
Sometimes it is necessary to get a more precise type for an expression. This can be achived using the following pattern match.
696693

697694
```scala
698-
def f(exp: Expr[Any]) =
695+
def f(exp: Expr[Any])(given QuoteContext) =
699696
expr match
700697
case '{ $x: $t } =>
701698
// If the pattern match succeeds, then there is some type `T` such that

library/src/scala/internal/quoted/Matcher.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,8 @@ private[quoted] object Matcher {
227227
case (While(cond1, body1), While(cond2, body2)) =>
228228
cond1 =?= cond2 && body1 =?= body2
229229

230-
case (New(tpt1), New(tpt2)) =>
231-
tpt1 =?= tpt2
230+
case (New(tpt1), New(tpt2)) if tpt1.tpe.typeSymbol == tpt2.tpe.typeSymbol =>
231+
matched
232232

233233
case (This(_), This(_)) if scrutinee.symbol == pattern.symbol =>
234234
matched

library/src/scala/quoted/Expr.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ class Expr[+T] private[scala] {
1818
*/
1919
final def getValue[U >: T](given qctx: QuoteContext, valueOf: ValueOfExpr[U]): Option[U] = valueOf(this)
2020

21+
/** Return the value of this expression.
22+
*
23+
* Emits an error error and throws if the expression does not contain a value or contains side effects.
24+
* Otherwise returns the value.
25+
*/
26+
final def value[U >: T](given qctx: QuoteContext, valueOf: ValueOfExpr[U]): U =
27+
valueOf(this).getOrElse(qctx.throwError(s"Expected a known value. \n\nThe value of: $show\ncould not be recovered using $valueOf", this))
28+
2129
/** Pattern matches `this` against `that`. Effectively performing a deep equality check.
2230
* It does the equivalent of
2331
* ```

library/src/scala/quoted/QuoteContext.scala

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class QuoteContext(val tasty: scala.tasty.Reflection) {
2222
tpe.unseal.show(syntaxHighlight)
2323
}
2424

25-
/** Report an error */
25+
/** Report an error at the position of the macro expansion */
2626
def error(msg: => String): Unit = {
2727
import tasty.{_, given}
2828
tasty.error(msg, rootPosition)(given rootContext)
@@ -34,6 +34,17 @@ class QuoteContext(val tasty: scala.tasty.Reflection) {
3434
tasty.error(msg, expr.unseal.pos)(given rootContext)
3535
}
3636

37+
/** Report an error at the position of the macro expansion and throws a StopQuotedContext */
38+
def throwError(msg: => String): Nothing = {
39+
error(msg)
40+
throw new StopQuotedContext
41+
}
42+
/** Report an error at the on the position of `expr` and throws a StopQuotedContext */
43+
def throwError(msg: => String, expr: Expr[_]): Nothing = {
44+
error(msg, expr)
45+
throw new StopQuotedContext
46+
}
47+
3748
/** Report a warning */
3849
def warning(msg: => String): Unit = {
3950
import tasty.{_, given}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package scala.quoted
2+
3+
/** Stop code generation after an error has been reported */
4+
class StopQuotedContext extends Throwable

0 commit comments

Comments
 (0)