-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Conversation
There was a problem hiding this 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:
- Separate subject from body with a blank line
- When fixing an issue, start your commit message with
Fix #<ISSUE-NBR>:
- Limit the subject line to 72 characters
- Capitalize the subject line
- Do not end the subject line with a period
- Use the imperative mood in the subject line ("Add" instead of "Added")
- Wrap the body at 80 characters
- Use the body to explain what and why vs. how
adapted from https://chris.beams.io/posts/git-commit
Have an awesome day! ☀️
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. |
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)}") | ||
} | ||
} | ||
|
There was a problem hiding this comment.
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
}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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))
There was a problem hiding this comment.
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
.
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 |
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 At the moment the strategy I'll be testing when I get back to coding this is to project out the I'll also look for some more test cases with unusual ascribed type trees that refer to a function that can be beta-reduced. |
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). |
@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. |
@LPTK I'll make sure to include tests for that. |
@nicolasstucki I actually had no idea about the member selection issue, I just didn't know about |
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`.
d95f8f9
to
0c34994
Compare
Here is a new version taking all we've discussed so far (and some other fun details/edge cases) into account. |
Thank you @fhackett |
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] |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 }
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 = ???
}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Continued on #8290
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.