Skip to content

Fix #8250: Allow Expr.betaReduce to drop type ascriptions. #8251

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

Merged
merged 1 commit into from
Feb 11, 2020

Conversation

fhackett
Copy link
Contributor

@fhackett fhackett commented Feb 8, 2020

Review by @nicolasstucki.

When a blackbox inline def is inlined, a type ascription is added to preserve the apparent return type of the inline def. Prior to this commit, the quoted.Expr.betaReduce function was unable to properly inline a known lambda expression coming from an inline def as it would see the type ascription and give up.

This change instead makes betaReduce drop type ascriptions, allowing these two features to interact properly.

Testing is done by verifying that betaReduce can correctly reduce the application of the result of an inline def to a constant value, with the reduction result being printed using the compiletime.code interpolator.

Consideration: dropping type ascriptions might change observable typing behaviour in some edge cases - it certainly changes one of the existing test results (see diff below). While you will certainly get some type ascription back as this code can only be run from the body of an inline method (which will put back an ascription at the end), I'm not sure if that's good enough.

Since the proposed change is so small I figured I would start a discussion by submitting this PR.

Copy link
Member

@dottybot dottybot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, and thank you for opening this PR! 🎉

All contributors have signed the CLA, thank you! ❤️

Commit Messages

We want to keep history, but for that to actually be useful we have
some rules on how to format our commit messages (relevant xkcd).

Please stick to these guidelines for commit messages:

  1. Separate subject from body with a blank line
  2. When fixing an issue, start your commit message with Fix #<ISSUE-NBR>:
  3. Limit the subject line to 72 characters
  4. Capitalize the subject line
  5. Do not end the subject line with a period
  6. Use the imperative mood in the subject line ("Add" instead of "Added")
  7. Wrap the body at 80 characters
  8. Use the body to explain what and why vs. how

adapted from https://chris.beams.io/posts/git-commit

Have an awesome day! ☀️

@sjrd
Copy link
Member

sjrd commented Feb 8, 2020

Consideration: dropping type ascriptions might change observable typing behaviour in some edge cases - it certainly changes one of the existing test results (see diff below). While you will certainly get some type ascription back as this code can only be run from the body of an inline method (which will put back an ascription at the end), I'm not sure if that's good enough.

The crucial question is: can it change the elaboration of code calling that inline def? If yes (even in one edge case), then it is not acceptable.

@nicolasstucki
Copy link
Contributor

There is one type ascription that should critically not be dropped. But that one is around the code inserted by the macro expansion. The fire this should not affect it. I will double check it.

println(code"${Macros.betaReduce(dummy)(3)}")
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could have a simple neg test tests/neg-macros/beta-reduce-inline-result/Test_2.scala (using a copy of beta-reduce-inline-result/Macro_1.scala) to check that the elaboration is not changed.

import scala.compiletime._

object Test {

  inline def dummy: Int => Int =
    (i: Int) => i + 1
  
  val x1: Int = Macros.betaReduce(dummy)(3)
  val x2: 4   = Macros.betaReduce(dummy)(3) // error
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, I will add this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Funny detail: this test actually passes already because the inline expansion adds its own type ascription. I'll look for a test that properly exercises the case where betaReduce's own type ascription is needed...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... actually @LPTK 's test should handle this. Thanks @LPTK 👍

@@ -2043,6 +2043,8 @@ class ReflectionCompilerInterface(val rootContext: core.Contexts.Context) extend
val argVals = argVals0.reverse
val argRefs = argRefs0.reverse
def rec(fn: Tree): Tree = fn match {
case Typed(expr, tpt) =>
rec(expr)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably get the return type out from tpt and add it as an ascription to the result. Even it the other tests pass, this would preserve the original typing of the inner expressions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that sounds good. See my comment below for more thoughts (which I'll try when I get back to coding this).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be somthing like

Typed(rec(expr), TypeTree(tpt.tpe.finalResult))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the hint - I did not know about finalResult.

@LPTK
Copy link
Contributor

LPTK commented Feb 8, 2020

In Scala, in general you can't remove type ascriptions and expect the code to still type check.

Here is a small variation on your test:

import scala.quoted._

object Macros {
  inline def betaReduce[Arg](inline fn : Arg=>Int)(inline arg: Arg): Int =
    ${ betaReduceImpl('{ fn })('{ arg }) }

  def betaReduceImpl[Arg](fn: Expr[Arg=>Int])(arg: Expr[Arg])(using qctx : QuoteContext): Expr[Int] =
    '{${Expr.betaReduce(fn)(arg)} + 1}
}
import scala.compiletime._

object Test {

  inline def dummy: Int => Int =
    (i: Int) => ???

  def main(argv : Array[String]) : Unit = {
    println(code"${Macros.betaReduce(dummy)(3)}")
  }
}

It results in:

-- Error: tests/run-macros/beta-reduce-inline-result/Test_2.scala:9:43 -------------------------------------------------
9 |    println(code"${Macros.betaReduce(dummy)(3)}")
  |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |            undefined: {
  |              {
  |                {
  |                  {
  |                    ???
  |                  }
  |                }
  |              }
  |            }.+ # 10336: TermRef(TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Nothing),+) at typer
  | This location contains code that was inlined from Macro_1.scala:9

@fhackett
Copy link
Contributor Author

fhackett commented Feb 8, 2020

Thanks for the counter-examples. Clearly we need to replace the type ascription rather than remove it entirely - I'm currently trying to figure out how to compute the correct type to put back. Since we can't expect the ascribed type tree to be exactly AppliedType(Functionx, List(arg1, ..., result)), I've been looking up compiler internals I can use to reliably get the right type even when type abstractions are involved.

At the moment the strategy I'll be testing when I get back to coding this is to project out the tpt's apply method's MethodType, then catch the return type from there.

I'll also look for some more test cases with unusual ascribed type trees that refer to a function that can be beta-reduced.

@nicolasstucki
Copy link
Contributor

Don't mind to much the member selection issue. Though adding the type ascription back will probably fix it. That is a more general issue (#7110).

@LPTK
Copy link
Contributor

LPTK commented Feb 9, 2020

@nicolasstucki is it really a special case of #7110? Solving #7110 would not necessarily solve this. Note that field selections are not the only things that can break when dropping type ascriptions. Another example is ascription to regain a transitive subtyping relationship that's otherwise not known to Dotty.

@fhackett you should also watch out for/test with dependent function types.

@fhackett
Copy link
Contributor Author

fhackett commented Feb 9, 2020

@LPTK I'll make sure to include tests for that.

@fhackett
Copy link
Contributor Author

fhackett commented Feb 9, 2020

@nicolasstucki I actually had no idea about the member selection issue, I just didn't know about Type.finalResult, which I will try rather than the construction with apply that I mentioned before. Interesting to learn about the other issue though.

When a blackbox inline def is inlined, a type ascription is added to preserve the apparent return type
of the inline def. Prior to this commit, the quoted.Expr.betaReduce function was unable to properly
inline a known lambda expression coming from an inline def as it would see the type ascription and
give up.

This change makes betaReduce rewrite type ascriptions, allowing it to inline type-ascribed functions such as those coming
from inlined defs.

- When type ascriptions are not present, behaviour does not change.
- When type ascriptions are encountered, the outermost ascription is kept.
- If the inline is successful, then the result type of the ascribed function type is ascribed to the inlined body.
- If the inline is not successful, the outermost ascribed type is re-ascribed to the non-inlineable expression before
  inserting a call to that expression.

There are several test cases, covering:
- verifying that betaReduce can correctly reduce the application of the result of an inline def
  to a constant value, with the reduction result being printed using the `compiletime.code` interpolator.
- verifying that the result type ascription handles dependent function types
- inserting ??? as either the inlined body or a function expression that cannot be inlined compiles without error (by
  re-ascribing the apparent type)
- a neg-test that a function whose type is `Int=>Int` and whose body inlines to `4` does not typecheck against the
  singleton type `4`.
@fhackett
Copy link
Contributor Author

Here is a new version taking all we've discussed so far (and some other fun details/edge cases) into account.

@nicolasstucki
Copy link
Contributor

Thank you @fhackett

@nicolasstucki nicolasstucki merged commit a29178c into scala:master Feb 11, 2020
topAscription match {
case Some(tpt) =>
// we assume the ascribed type has an apply that has a MethodType with a single param list (there should be no polys)
val methodType = tpt.tpe.member(nme.apply).info.asInstanceOf[MethodType]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fhackett won't this crash if the ascribed type has a val apply member that's not a method? I think this could happen if the term passed as fn is not really a function, or if the apply is overloaded. Something like:

scala> object test extends (String => String) { val apply = (x:Int) => x; def apply(x: String) = x }
// defined object test

scala> test.apply("abc")
val res0: String = abc

scala> test.apply
val res1: Int => Int = test$$$Lambda$1268/578362229@6f3bd37f

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case is only reached if the prefix is an explicit closure.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I had missed the closureDef pattern. But it seems even a Closure can have a type that contains an overloaded apply member: indeed it can be any SAM type, no?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, I will have to check if we can get one of those here. @LPTK did you have a specific example in mind?

Copy link
Contributor

@LPTK LPTK Feb 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like betaReduce('{ (x => x) : Test }, ...) where we have:

abstract class Test extends (String => String) { val apply = 123 }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried it, and we get a class cast exception.
I can figure out a fix - should I push a second commit here or open a second issue+PR?

Copy link
Contributor Author

@fhackett fhackett Feb 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I can do one "better". See this fun edge case:

abstract class Test extends (Int => Int) {
  def impl(i: Int): Int
  def apply(i: Int): Int = ???
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is getting more involved than just a quick fix, so I'll open a fresh issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Continued on #8290

@fhackett fhackett deleted the fhackett-fix-8250 branch February 11, 2020 17:05
@anatoliykmetyuk anatoliykmetyuk added this to the 0.23.0-RC1 milestone Mar 12, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Expr.betaReduce can't reduce past the type ascription from inline def expansion
6 participants