Skip to content

Missing ability to check whether inline argument is constant #11780

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
japgolly opened this issue Mar 17, 2021 · 3 comments
Closed

Missing ability to check whether inline argument is constant #11780

japgolly opened this issue Mar 17, 2021 · 3 comments

Comments

@japgolly
Copy link
Contributor

japgolly commented Mar 17, 2021

I'd be happy to contribute a PR to fix if necessary.

The inline doc says

when called with a constant exponent n, the following method for power will be implemented by straight inline code without any loop or recursion.

and then goes on to show this example:

inline def power(x: Double, n: Int): Double =
   if n == 0 then 1.0
   else if n == 1 then x
   else
      val y = power(x, n / 2)
      if n % 2 == 0 then y * y else y * y * x

power(expr, 10)
// translates to
//
//    val x = expr
//    val y1 = x * x   // ^2
//    val y2 = y1 * y1 // ^4
//    val y3 = y2 * x  // ^5
//    y3 * y3          // ^10

So if I understand correctly,

  1. n == 0 and n == 1 occur at compile-time and only match constants. def p(n: Int) = power(expr,n); p(10) wouldn't translate to the example translation above because n would be a dynamic reference rather than a constant.
  2. scalac allows comparisons of arguments to constants like 1 and 0 because they're constants in the inline function body

All well and good but what if one wants to simply know whether n is a constant or not (regardless of the value)?

In my case I was trying to determine whether a String argument is constant or not.
This doesn't work:

  inline given blah(inline body: String): Name =
    inline match n
      case _: Singleton => Name.now(body)
      case _            => Name.lazily(body)

The methods in scala.compiletime don't seem to help because they're more about types.

My own workaround was to fallback to the quoting API like this:

  inline given blah(inline body: String): Name =
    ${ _blah('body) }

  private def _blah(expr: Expr[String])(using Quotes): Expr[Name] = {
    import quotes.reflect.*
    expr.asTerm match {
      case Inlined(_, _, Literal(StringConstant(s))) => '{ Name.now($expr) }
      case _                                         => '{ Name.lazily($expr) }
    }
  }

It'd be nice to be able to make this work without falling back to quotes. This ability doesn't seem to exist yet, nor does it seem hard to write. I'd be happy to write a PR to add this. Maybe a bunch of methods like inline def stringConstantOpt(inline s: String): Option[String] for each constant type?

@nicolasstucki
Copy link
Contributor

nicolasstucki commented Mar 17, 2021

This is exactly what the first version of scala.compiletime.requireConst #9764 was talking. Thought we ended up with a simpler version that does not cover all these cases.

This kind of code is more suited for macros than inline code in general.

But to have this functionality in inline code we could define a macro that looks something like this

inline def isConstString(inline str: String): Boolean = ${ impl('str) }
private def impl(str: Expr[String])(using Quotes): Expr[Boolean] = 
  Expr(str.value.isDefined)
  // or
  // str.value match { case Some(_) => true ; case _ => false } 
  // or
  // str match { case Expr(_) => true ; case _ => false }

Now constString can be used in inlined code

inline given blah(inline body: String): Name =
    inline if isConstString(body) then Name.now(body)
    else Name.lazily(body)

We could add scala.compiletime.isConst, but I would try to avoid it and make users use macros instead as there are fewer ways they could misuse the abstractions and end up with duplicated code. As my example shows, if used correctly the macro version can be quite small and explicit while providing the capability of debugging the code while the macro is expanded.

@nicolasstucki
Copy link
Contributor

FYI @japgolly the macro you wrote can be simplified considerably using the Expr.value. Note that this is an extension method located in Quotes.

-  private def _blah(expr: Expr[String])(using Quotes): Expr[Name] = {
-    import quotes.reflect.*
-    expr.asTerm match {
-      case Inlined(_, _, Literal(StringConstant(s))) => '{ Name.now($expr) }
-      case _                                         => '{ Name.lazily($expr) }
-    }
-  }
+  private def _blah(expr: Expr[String])(using Quotes): Expr[Name] = 
+    if expr.value.isDefined then '{ Name.now($expr) } else '{ Name.lazily($expr) }

@nicolasstucki
Copy link
Contributor

I would avoid adding this capability to the library for 3.0. Anyone who requires it in inline can implement the macro in 3 lines of code. If we see that is really useful to have we can consider adding it in a later version of scala 3.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants