Skip to content

Change retains annotation from using term arguments to using type arguments #22909

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

noti0na1
Copy link
Member

@noti0na1 noti0na1 commented Apr 2, 2025

Fix #22842

This PR updates the retains annotation to use type arguments instead of term arguments in order to streamline capture set representations and related type checking. Key changes include:

  • Adjusting the looking up for capture variables in capture sets, by creating dummy term symbols.
  • Removing term parameter for retains annotation and capsOf.
  • Modifying annotation construction in RetainingType, CaptureAnnotation, and associated AST desugarings.

For example,

def f[C^](x: A^): B @retains[x.type | C] // B^{x, C}

@noti0na1 noti0na1 marked this pull request as ready for review May 15, 2025 20:28
@noti0na1 noti0na1 self-assigned this May 15, 2025
@noti0na1 noti0na1 requested a review from Copilot May 15, 2025 20:28
Copilot

This comment was marked as resolved.

@noti0na1 noti0na1 requested a review from Copilot May 26, 2025 03:19
Copilot

This comment was marked as resolved.

@noti0na1 noti0na1 requested review from odersky and bracevac May 26, 2025 11:32
@noti0na1 noti0na1 assigned odersky and unassigned noti0na1 May 26, 2025
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.

The refactoring of annotations looks all good.

I have doubts about the CaptureParam part, for several reasons. I think we should try to

  • drop the InCaptureSet mode
  • Re-use the ConstructorProxy flag (maybe call it Surrogate instead to make clear it serves multiple purposes now)
  • Set proxy types to NoType at PostTyper
  • Avoid all special treatments in transforms

@@ -380,6 +380,8 @@ object Flags {
/** Tracked modifier for class parameter / a class with some tracked parameters */
val (Tracked @ _, _, Dependent @ _) = newFlags(46, "tracked")

val (CaptureParam @ _, _, _) = newFlags(47, "capture-param")
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't have many flags left, and a flag in this range would have to be pickled, which means a change in TastyFormat. I think we should just re-use the ConstructorProxy flag, which serves a very similar purpose, and is already thoroughly debugged.

@@ -748,7 +748,9 @@ object Types extends TypeUtils {
case tp: ClassInfo => tp.appliedRef
case _ => widenIfUnstable
}
findMember(name, pre, required, excluded)
// The dummy term capture variable can only be found in a capture set.
val excluded1 = if ctx.mode.is(Mode.InCaptureSet) then excluded else excluded | CaptureParam
Copy link
Contributor

Choose a reason for hiding this comment

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

It's an interesting twist, but I am not sure we need the mode either. I believe it would complicate the spec if we had to say that a capability is present as a term name only when we are looking inside a set. Analogously, we'd say a constructor proxy is available as a term name only when we are resolving the function part of an application. We could do that as well, but we don't because it would complicate the spec. I think it's OK to just unconditionally look for capature parameter terms.

Copy link
Member Author

@noti0na1 noti0na1 May 27, 2025

Choose a reason for hiding this comment

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

Eventually all dummy term symbols will be erased, and I don't want them been found as a regular value and left in the program. For example, in tests/neg-custom-args/captures/check-dummy-symbol.scala

@@ -196,7 +196,10 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
val saved = inJavaAnnot
inJavaAnnot = annot.symbol.is(JavaDefined)
if (inJavaAnnot) checkValidJavaAnnotation(annot)
try transform(annot)
try
val annotCtx = if annot.hasAttachment(untpd.RetainsAnnot)
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder why this is necessary. We should not do any name resolution here?

Copy link
Member Author

Choose a reason for hiding this comment

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

PostTyper will check all singleton types are valid, and we want to treat singletons like f: => T specially in capture set.

Copy link
Contributor

Choose a reason for hiding this comment

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

ok I see now

@@ -89,7 +89,7 @@ class Getters extends MiniPhase with SymTransformer { thisPhase =>
else d1 = d1.copySymDenotation(initFlags = d1.flags &~ Local)
d1
}
private val NoGetterNeededFlags = Method | Param | JavaDefined | JavaStatic
private val NoGetterNeededFlags = Method | Param | JavaDefined | JavaStatic | CaptureParam
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this necessary? We should not even see capture parameters at this point. Maybe hide them in PostTyper as is done in #23269.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agree, we should remove these special symbols right after typer, then maybe we don't all the special handling later.

@@ -801,7 +801,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
case CapturingType(_, refs) =>
!refs.isAlwaysEmpty
case RetainingType(parent, refs) =>
!refs.isEmpty
!refs.retainedElements.isEmpty
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm wondering if we should massage the RetainingType extractor to directly expose refs as the retainedElements set? Isn't this what we want 99% of the time anyways?

Copy link
Member Author

Choose a reason for hiding this comment

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

RetainingType is designed to work only on type level, without additional processing and check. There is a place we only check what annotation it is and pass refs back directly, and there are places we don't want illegal capture reference exception, like checking well forming in typer and printing.

@@ -1330,8 +1326,8 @@ class Definitions {
*/
object ByNameFunction:
def apply(tp: Type)(using Context): Type = tp match
case tp @ RetainingType(tp1, refs) if tp.annot.symbol == RetainsByNameAnnot =>
RetainingType(apply(tp1), refs)
case tp @ RetainingType(tp1, refSet) if tp.annot.symbol == RetainsByNameAnnot =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Are RetainingTypes corresponding to what we in the papers on capture checking refer to as CapturingTypes? If yes, let's rename it (same rationale as refactoring CaptureRef to Capability).

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure I understand the comment. RetainingType is just a "CapturingType" living outside the cc phase.

Copy link
Contributor

Choose a reason for hiding this comment

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

The point is to find a better name. Just looking at it, the name tells me nothing. And if it's something that corresponds to the terminology in the calculus, then we should name it as such. Or at the very least, it should be a name that would tell us it's capture-checking related.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's RetainingType since it's of the form T @retains U.

Copy link
Contributor

Choose a reason for hiding this comment

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

Same thing with @retains: is this truly the best name for what it stands for? Why not, e.g., T @captures U, T @capturing U, etc. ?

Copy link
Member Author

@noti0na1 noti0na1 May 27, 2025

Choose a reason for hiding this comment

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

I don't mind if there is a better name. Since it's an internal temporary type holder not related to the core calculus, users wouldn't see it, and most of cc logic wouldn't see it neither, the name is just not important here. Using a special name also makes it easy to tell a type is newly coming to cc phase.

@@ -2,7 +2,7 @@ import language.experimental.captureChecking
import caps.*

trait Abstract[X^]:
type C >: {X}
type C^ >: {X}
Copy link
Contributor

Choose a reason for hiding this comment

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

As the name of the file suggests, the point of this test is to check if we can omit the hat. I know we discussed that there are situations that cause trouble like C >: A <: B, but here (and also the other test cases) I don't quite see why it's strictly necessary to re-add the hat.

@@ -8,6 +8,8 @@ import annotation.tailrec
import caps.cap
import caps.unsafe.unsafeAssumeSeparate

import language.experimental.captureChecking
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe just uniformly add this import to all tests in the captures test folders? It's annoying to not have it during manual debugging.

Copy link
Contributor

@bracevac bracevac left a comment

Choose a reason for hiding this comment

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

Some smaller comments. LGTM otherwise. I'll leave the final approval to Martin.

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.

Change representation of retains annotation?
3 participants