Skip to content
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

Merged
merged 9 commits into from
Jun 17, 2021

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Jun 13, 2021

Fixes #12814

@odersky
Copy link
Contributor Author

odersky commented Jun 13, 2021

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.

@soronpo
Copy link
Contributor

soronpo commented Jun 13, 2021

Instead of restricting inlines completely, maybe just disallow the automatic "casting" to the opaque within the inline?
If I use an opaque type to create a new type Positive and I have an inline method that verifies the value, there is no reason I shouldn't be able to use .asInstanceOf instead:

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 asInstanceOf[Positive], but this is a far better restriction than eliminating any inline definition within the scope of an opaque.

@smarter
Copy link
Member

smarter commented Jun 13, 2021

But it looks as hard as ever.

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.

@odersky
Copy link
Contributor Author

odersky commented Jun 14, 2021

@smarter I did a first test with your idea which worked! So I want to follow up on this approach before merging this PR.

@odersky odersky marked this pull request as draft June 14, 2021 10:16
Introduce a third boundary for stopping at packages
@odersky odersky force-pushed the fix-opaque-inline branch from b1f070e to 9a998d4 Compare June 14, 2021 16:58
odersky added 4 commits June 14, 2021 23:48
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
@odersky odersky marked this pull request as ready for review June 15, 2021 11:14
@odersky odersky requested a review from smarter June 15, 2021 11:14
@odersky odersky assigned smarter and odersky and unassigned smarter and odersky Jun 15, 2021
@odersky odersky changed the title Plug loophole for "no inlines with opaques" rule Drop "no inlines with opaques" implementation restriction Jun 15, 2021
Comment on lines 929 to 934
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.
Copy link
Member

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))
Copy link
Member

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

Copy link
Contributor Author

@odersky odersky Jun 16, 2021

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.

@smarter smarter assigned odersky and unassigned smarter Jun 15, 2021
/** 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)

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.

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'll change it.

tests/pos/opaque-inline.scala Show resolved Hide resolved
@odersky odersky force-pushed the fix-opaque-inline branch from 27ae1d7 to 3f07fb9 Compare June 16, 2021 11:17
@odersky odersky removed their assignment Jun 16, 2021
@smarter smarter assigned odersky and unassigned smarter Jun 16, 2021
@odersky odersky assigned smarter and unassigned odersky Jun 16, 2021
Copy link
Member

@smarter smarter left a comment

Choose a reason for hiding this comment

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

🎆

@smarter smarter enabled auto-merge June 17, 2021 14:31
@smarter smarter merged commit 20c84d6 into scala:master Jun 17, 2021
@smarter smarter deleted the fix-opaque-inline branch June 17, 2021 15:00
@odersky odersky added the release-notes Should be mentioned in the release notes label Aug 30, 2021
bblfish added a commit to bblfish/banana-play that referenced this pull request Aug 30, 2021
@Kordyjan Kordyjan added this to the 3.0.2 milestone Aug 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release-notes Should be mentioned in the release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Loophole in "no inlined with opaque" checking
7 participants