-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Drop "no inlines with opaques" implementation restriction #12815
Conversation
A more positive fix of the problem would drop the implementation restriction that inline methods cannot be defined where opaque types are visible. But it looks as hard as ever. Expanding inlines will render code type-incorrect as #12814 shows. We only eliminate opaque types some 20 phases later (after that the program would be type-correct again). And we cannot move elim opaques earlier since RefChecks assumes that opaque types are still opaque. |
Instead of restricting inlines completely, maybe just disallow the automatic "casting" to the opaque within the inline? opaque type Positive <: Int = Int
object Positive:
inline def apply(inline value : Int) : Positive =
inline if (value <= 0) compiletime.error("must be positive")
else value.asInstanceOf[Positive]
inline def justAnotherInlineDef : Unit = ??? //this is also disallowed :-( Ideally, we wouldn't want |
We've briefly discussed before if we could handle opaques in inline by giving a more precise type to the this-proxy we generate, but I don't think anyone has actually tried it so far. Taking #12814 as an example, we get after inlining: val nine: refined.Positive =
{
val PositiveFactory_this: refined.Positive.type = refined.Positive
PositiveFactory_this.f(x):refined.Positive
} If we could instead generate: val nine = {
val r1: refined.type & { type Positive = Int } = refined.asInstanceOf[ refined.type & { type Positive = Int }]
val PositiveFactory_this: r1.Positive.type = r1.Positive
PositiveFactory_this.f(x)
} Then the expansion would typecheck. |
@smarter I did a first test with your idea which worked! So I want to follow up on this approach before merging this PR. |
Introduce a third boundary for stopping at packages
b1f070e
to
9a998d4
Compare
The idea is described in part in the comments to PR scala#12815. 1. If a this-proxy definition contains references to classes that see opaque types, replace those references by other proxies that expose the opaque alias in a refinement type of the original reference type. 2. Make sure this proxies exist even for static moules containing opaque types, so that step 1 can be applied. 3. With steps 1 and 2, we can drop the restriction that inline methods may not be defined where opaque aliases are visible.
The lack of this caused a spurious pickling error in i11914a.scala when proxy generation changed.
We don't need a thos proxy if the target is a statioc object, unless that object sees opaque aliases. This allows to simplify the registerType logic, and avoided some spurious proxy generations, for instance in run-macros/i5119
if opaqueProxies.nonEmpty && !inlinedMethod.is(Transparent) then | ||
expansion0.cast(call.tpe)(using ctx.withSource(expansion0.source)) | ||
// the cast makes sure that the sealing with the declared type | ||
// is type correct. Without it we might get problems since the | ||
// expression's type is the opaque alias but the call's type is | ||
// the opaque type itself. An example is in pos/opaque-inline1.scala. |
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 branch is never actually reached for pos/opaque-inline1.scala
, it is reached for pos/opaque-inline.scala
but at that point expansion0.tpe
and call.tpe
are both refined.Positive
so the cast doesn't do anything (and the test passes if I delete it).
case RefinedType(parent, rname, TypeAlias(alias)) => | ||
val opaq = cls.info.member(rname).symbol | ||
if opaq.isOpaqueAlias then | ||
(rname, alias.stripLazyRef.asSeenFrom(ref, cls)) |
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.
Minor point: we could check the accessibility of alias
at this point to generate a slightly better error message if it isn't accessible, but the current one isn't too bad:
object refined:
private class Internal(x: Int)
opaque type Positive = Internal
inline def Positive(value: Int): Positive = new Internal(value)
object test:
def run: Unit =
val x = 9
val nine = refined.Positive(x)
-- Error: tests/pos/opaque-inline1.scala:10:31 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
10 | val nine = refined.Positive(x)
| ^^^^^^^^^^^^^^^^^^^
| class Internal cannot be accessed as a member of (refined : refined.type{Positive = refined.Internal}) from module class test$.
| This location contains code that was inlined from opaque-inline1.scala:4
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.
There was a wrong condition for eliding the proxies, which is fixed in 2414033. Now pos/opaque-inline1 does not compile anymore if I remove the cast. It fails in -Ycheck with
java.lang.AssertionError: assertion failed: Found: Int
Required: refined.Positive
found: class Int in package scala with class Int, flags = final abstract <scala-2.x> <touched>, underlying = Int, AnyVal {...}
expected: type Positive in object refined with type Positive, flags = <deferred> opaque <touched>, underlying = refined.Positive, , Any, {...}
tree = {
val $proxy1: refined.type{Positive = Int} =
refined.$asInstanceOf[refined.type{Positive = Int}]
val refined$_this: ($proxy1 : refined.type{Positive = Int}) = $proxy1
refined$_this.f(x):refined$_this.Positive
} while compiling opaque-inline1.scala
The weird thing is that with the original condition everything succeeded, even though the expansion was then
val x = 9
val nine = refined.f(x): refined.Positive
This is clearly type-incorrect, since 9
is an Int
and refined.f
expects a refined.Positive
. Indeed, if I write this expansion manually, it fails.
-- [E007] Type Mismatch Error: opaque-inline1.scala:12:25 ----------------------
12 | val nine = refined.f(x): refined.Positive
| ^
| Found: (x : Int)
| Required: refined.Positive
But somehow -Ycheck
does not test this. I traced Ycheck's behavior and it seems to think that refined.f: Int => Int
even though it should be refined.f: Positive => Positive
. The reason for this is that it copies the tree refined.f
as is from the inline method, which is in a scope where Positive = Int
. Hence, Int => Int
is a legal type for f
there. It's the copying that is wrong, and unfortunately Ycheck does not catch it.
But the case won't arise anymore with this PR.
/** Map first halfs of opaqueProxies pairs to second halfs, using =:= as equality */ | ||
def mapRef(ref: TermRef): Option[TermRef] = | ||
opaqueProxies | ||
.find((from, to) => from.symbol == ref.symbol && from =:= ref) |
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.
you can also write:
opaqueProxies
.collectFirst { case (from, to) if from.symbol == ref.symbol && from =:= ref => to }
and I think not have two lambdas.
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'll change it.
27ae1d7
to
3f07fb9
Compare
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.
🎆
Fixes #12814