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

Canonicalize capture variable subtype comparisons #22299

Merged
merged 3 commits into from
Jan 22, 2025

Conversation

bracevac
Copy link
Contributor

@bracevac bracevac commented Jan 2, 2025

Fixes #22103

Subtype problems where at least one side is a type variable representing a capture variable are canonicalized to capturing type comparisons on the special CapSet for the universe capture sets. For example, C <: CapSet^{C^} becomes CapSet^{C^} <: CapSet^{C^}, and A <: B becomes CapSet^{A^} <: CapSet^{B^} if both A and B are capture variables.

Supersedes #22183 and #22289. This solution is overall cleaner and does not require adding a new bit to the TypeComparer's ApproxState.

TODOs/Issues/Questions:

  • Fix extension method in test cc-poly-varargs.scala. Currently causes an infinite regress.
    • Fix the aftermath
      • tests/neg-custom-args/captures/lazylists-exceptions.scala
      • tests/neg-custom-args/captures/exceptions.scala
      • tests/neg-custom-args/captures/real-try.scala
      • tests/run-custom-args/captures/colltest5
  • Some negative cases in test capture-vars-subtyping.scala pass: D <: E fails, but its canonicalized form CapSet^{D^} <: CapSet^{E^} now succeeds. Potential problem in the subcapturing implementation.
  • Extend to intersection/unions def f[C^, D^, E <: C | D, F <: C & D](...) = ... etc. Lacking good uses cases, not planned right now.
  • If we have C^ declared in the current context, should there be a difference between C vs. C^ for subsequent mentions? We currently do, but seems a bit too subtle for users. Will be addressed by a new scheme for declaring capture variables using context bounds.

@bracevac
Copy link
Contributor Author

bracevac commented Jan 2, 2025

Moved to dotty @odersky

@bracevac bracevac force-pushed the capture-subtyping-canon branch 2 times, most recently from 2770688 to bb1b215 Compare January 19, 2025 21:15
@bracevac
Copy link
Contributor Author

bracevac commented Jan 20, 2025

Notes on the extension-method problem:

The example in cc-poly-varargs.scala seems to loop infinitely on extrapolateCaptureRef in CaptureSet.scala.

However, a simpler version of it does not diverge, but reveals another problem (not clear if it is related):

import language.experimental.captureChecking

abstract class Source[+T, Cap^] 

extension [T, Cap^](src: Source[T, Cap]^)
  def transformValuesWith[U](f: (T -> U)^{Cap^}): Source[U, Cap]^{src, f} = ???

def simpler[T, Cap^](src: Source[T, Cap]^{Cap^}): Source[T, Cap]^{Cap^} = // <- need to annotate the explicit return type, otherwise error
  src.transformValuesWith(identity)

If we leave out the return type of simpler, we get the error

-- [E007] Type Mismatch Error: local/cc-poly-varargs.scala:12:74 ---------------
12 |def simpler[T, Cap^](src: Source[T, Cap]^{Cap^}) = src.transformValuesWith(identity)
   |                                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |         Found:    Source[box T^?, caps.CapSet^{Cap^}]^{src}
   |         Required: Source[T, caps.CapSet]
   |
   |         Note that the expected type Source[T, caps.CapSet]
   |         is the previously inferred result type of method simpler
   |         which is also the type seen in separately compiled sources.
   |         The new inferred type Source[box T^?, caps.CapSet^{Cap^}]^{src}
   |         must conform to this type.
   |
   | longer explanation available when compiling with `-explain`

Edit: It looks like the initial problem that we had with race.

Edit2: This seems to be the expected behavior, we need to annotate the return type if it's impure.

@bracevac
Copy link
Contributor Author

bracevac commented Jan 21, 2025

A simpler example that exhibits the divergence:

import language.experimental.captureChecking

abstract class Source[+T, Cap^] 

def id[T, Cap^](src: Source[T, Cap]^): Source[T, Cap]^{src} = src
def promote[T, Cap^](src: Source[T, Cap]^{Cap^}): Source[T, Cap]^{Cap^} = src

def fun[T, Cap^](src: Source[T, Cap]^{Cap^}): Source[T, Cap]^{Cap^} =
  val res = id(src)
  promote(res)

Applying promote causes res to appear deeply in Source[T, CapSet^{Cap^,res}], and TypeOps.avoid gets hung up on that when removing res.
With the latest fix, we get it to terminate and can obtain an -Xprint:cc that clearly shows this:

...
def fun[T, Cap >: caps.CapSet <: caps.CapSet^](src: Source[T, Cap]^{Cap^}):
      Source[T, Cap]^{Cap^} =
      {
        val res: Source[box T^?, caps.CapSet^{Cap^, res}]^{src} =
          id[box T^?, caps.CapSet^{Cap^, res}](src)
        promote[box T^?, caps.CapSet^{res, Cap^}](res)
      }

We already have a cyclic reference at res: Source[box T^?, caps.CapSet^{Cap^, res}]^{src}!

But even with an explicit ascription

val res: Source[T, Cap]^{src} = ...

the last line will still carry res

def fun[T, Cap >: caps.CapSet <: caps.CapSet^](src: Source[T, Cap]^{Cap^}):
      Source[T, Cap]^{Cap^} =
      {
        val res: Source[T, Cap]^{src} = id[box T^?, caps.CapSet^{Cap^}](src)
        promote[box T^?, caps.CapSet^{res, Cap^}](res)
      }

@bracevac
Copy link
Contributor Author

Part of the problem seems to stem from subsumes: Cap does not subsume res in the implementation, but should it not hold because res reaches src which reaches Cap?

Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

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

Nice that the problem in AvoidMap got solved by the changes in subsumes. The changes make sense. I'd like to understand better why the AvoidMap problem poppsed up without these changes.

compiler/src/dotty/tools/dotc/cc/CaptureSet.scala Outdated Show resolved Hide resolved
compiler/src/dotty/tools/dotc/core/TypeComparer.scala Outdated Show resolved Hide resolved
compiler/src/dotty/tools/dotc/core/TypeComparer.scala Outdated Show resolved Hide resolved
Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

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

Also maybe squash the commits, since they tried several things that got reverted afterwards?

Fixes scala#22103

Subtype problems where at least one side is a type variable
representing a capture variable are canonicalized to
capturing type comparisons on the special `CapSet` for the
universe capture sets. For example, `C <: CapSet^{C^}`
becomes `CapSet^{C^} <: CapSet^{C^}`, and `A <: B` becomes
`CapSet^{A^} <: CapSet^{B^}` if both `A` and `B` are capture
variables.

Needs a patch in subsumes

Without the patch we get divergence in AvoidMap for cc-poly-varargs.
The underlying cause for the divergence is that we could not conclude
that the capture variable `Cap^` subsumes `left`
through `src1`, i.e., `left` reaches `Cap^`.
That caused `left` to appear circularly in its
own capture set and subsequently avoidance to loop
forever.
@odersky odersky force-pushed the capture-subtyping-canon branch from cef5116 to a587261 Compare January 22, 2025 14:15
@odersky
Copy link
Contributor

odersky commented Jan 22, 2025

I squashed all the commits into one. Will push some polishing changes on top.

Make tests in TypeComparer fit better with the rest
@bracevac bracevac enabled auto-merge January 22, 2025 17:12
@@ -119,6 +118,15 @@ trait CaptureRef extends TypeProxy, ValueType:
case info: SingletonCaptureRef => test(info)
case info: AndType => viaInfo(info.tp1)(test) || viaInfo(info.tp2)(test)
case info: OrType => viaInfo(info.tp1)(test) && viaInfo(info.tp2)(test)
case info @ CapturingType(_,_) if this.derivesFrom(defn.Caps_CapSet) =>
Copy link
Member

Choose a reason for hiding this comment

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

Why does this condition check this instead of info?

Copy link
Member

Choose a reason for hiding this comment

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

The case here does not look currect for me.viaInfo is used only if there is some equivalent relation from singleton types.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's tailored to the case that we want to check that a term variable reaches a capture variable (resp. the capture variable subsumes the term variable, so we check the this), because of the issue outlined in
#22299 (comment)
Not sure if this is best place to do it, but logically we have to chase along the infos of the context entries. The case y: TermRef if !y.isRootCapability => part will invoke this function as a last resort, and since we have to follow the info then, we might as well do it here.

Feel free to open a new PR if you see a better way to do it.

val a: C = ???
val b: CapSet^{C^} = a
val c: C = b
val d: CapSet^{C^, c} = a
Copy link
Member

Choose a reason for hiding this comment

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

In theory, we should never have an instance of CapSet. I don't think we need to test CapSet^{C^, c}, a singleton c with type CapSet inside a capture set. Because this is confusing to handle whether a CaptureRef is representing a value or its underlying capture set.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The point of this is to test subtyping relations between capture sets. The term bindings are a crutch. Back then I could not do it in the "obvious" way using e.g. summon[C <:< CapSet^{C^}], because resolution happens before capture checking.

Copy link
Member

Choose a reason for hiding this comment

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

I think the assignments are fine to test. I mean the c inside the capture set.

@bracevac bracevac merged commit b709262 into scala:main Jan 22, 2025
29 checks passed
@bracevac bracevac deleted the capture-subtyping-canon branch January 22, 2025 20:14
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.

Improve Subtyping and Normalization of Capture Sets
3 participants