Skip to content

Expression from a quote inconsistently available to splicing #16533

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
hmf opened this issue Dec 15, 2022 · 2 comments
Closed

Expression from a quote inconsistently available to splicing #16533

hmf opened this issue Dec 15, 2022 · 2 comments
Labels
area:metaprogramming:quotes Issues related to quotes and splices itype:invalid

Comments

@hmf
Copy link

hmf commented Dec 15, 2022

Compiler version

Scala version 3.2.1

Minimized code

While exploring macro quotes, an incorrect code snippet seems to exhibit inconsistent compilation and may provide a possible opportunity to detect and flag an error. The code is below. Also available in scastie.

import scala.quoted.*

transparent inline def power5(a: Int, b:Int): Any = ${ power5Impl('a,'b) }

def power5Impl(a: Expr[Int], b: Expr[Int])(using q1: Quotes): Expr[Int] =
  import quotes.reflect.*
  b match 
    case '{0} => 
      println(s"b(0) : ${b.show} = ${b.value}")
      '{1}
    case '{1} => 
      println(s"b(1) : ${b.show} = ${b.value}")
      a
    case '{ $expr: tpe } => 
      println(s"b(a) : ${b.show} = ${b.value}")
      println(s"b(b) : ${Type.show[tpe]} = ${expr.value}")
      val test: Expr[Int] = '{$b - 1}
      println(s"test : ${test.show} = ${test.value}")
      // No terminal match on 0 or 1. Stack overflow
      // val testOut: Expr[Int] = power5Impl(a, test)
      // println(s"testOut : ${testOut.show} = ${testOut.value}")
      '{
        val nb = $b - 1
        // Incorrect exponent value. Always None. No terminal match on 0 or 1. Stack overflow
        // This is correct. nb has not been calculated
        val next = ${ power5Impl(a, 'nb)}
        // No terminal match on 0 or 1
        // Matches and stops ok
        // val next = ${ power5Impl(a, Expr(1))}
        // Matches and stops ok
        // val next = ${ power5Impl(a, '{1})}
        // Correct exponent value. No terminal match on 0 or 1. Stack overflow
        // val next = ${ power5Impl(a, test)}
        $a * next
      }

Output

As is, the code will, as it should, cause stack overflow. The output is:

b(a) : 2 = Some(2)
b(b) : 2 = Some(2)
test : 2.-(1) = Some(1)
b(a) : nb = None
b(b) : nb.type = None
test : nb.-(1) = None
b(a) : nb = None
b(b) : nb.type = None
test : nb.-(1) = None
b(a) : nb = None
b(b) : nb.type = None
test : nb.-(1) = None

The issue is that the value for nb is not available when we splice the call to power5Impl(a, 'nb). Hence the value None. If we comment this line and remove the comment from the line:

        // val next = ${ power5Impl(a, test)}

then we also get a stack overflow. This time however, the value is updated correctly. The overflow occurs because matching fails and may be due to another issue (see #16521).

Expectation

In the case of the nb variable being used when unavailable, maybe this can be detected and flagged by compiler? If so a compilation may issue an error or at least a warning.

On this next issue, I am unsure.

In regards to the next that uses the test value. Intuitively it seems correct to expect this to be available when splicing. However, after seeing what the issue is with nb, I would expect it to behave just like the nb. After all both nb and test are the same quote. It seems like the compiler calculates test and makes it available, unlike the nb. Maybe the nb should also be made available?

@hmf hmf added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Dec 15, 2022
@nicolasstucki nicolasstucki added area:metaprogramming:quotes Issues related to quotes and splices and removed stat:needs triage Every issue needs to have an "area" and "itype" label labels Dec 18, 2022
@nicolasstucki
Copy link
Contributor

By generating val nb: Int = 4 - 1; power5Impl(a, nb) we ger into a situation where b will never be reduced and we always. Furthermore, having this b will force the match into the case '{ $expr: tpe } => as '{ nb: Int } and into an infinite loop. The other cases work because the value is directly placed as the b argument. This kind of bug in the user code is too hard (maybe impossible) to detect in the compiler.

Also, try to avoid partially evaluating values waiting generated code as this is slower and does not guarantee that the value will be reduced. The best practice is to use .value and then you will have all the control over how the code is unrolled. This also reduces the number of ways you could accidentally make a bug in the macro implementation. The ideal code would have been:

import scala.quoted.*

transparent inline def power5(a: Int, b:Int): Any = ${ power5Impl('a,'b) }

def power5Impl(a: Expr[Int], b: Expr[Int])(using Quotes): Expr[Int] =
  b.value match
    case Some(n) => power5Impl(a, n)
    case None =>
      // Not handled in the original example and source of infinite recursion
      ???

def power5Impl(a: Expr[Int], n: Int)(using Quotes): Expr[Int] =
  n match
    case 0 => '{1}
    case 1 => a
    case _ =>
      val nb = n - 1
      '{
        val next = ${ power5Impl(a, nb) }
        $a * next
      }
      // or just '{ $a * ${ power5Impl(a, n-1) } }

@hmf
Copy link
Author

hmf commented Dec 19, 2022

@nicolasstucki Thank you. Will make it my default to grab the value and use that. Maybe something to add to the best practices section?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:metaprogramming:quotes Issues related to quotes and splices itype:invalid
Projects
None yet
Development

No branches or pull requests

2 participants