Skip to content

Use level checking for Fresh instances #23101

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 17 commits into
base: main
Choose a base branch
from

Conversation

odersky
Copy link
Contributor

@odersky odersky commented May 5, 2025

This PR changes the way we treat fresh instances. Instead of disallowing all of them when instantiating type parameters (as well as in a number of other situations), we allow them but check that inner fresh instances do not get widened to outer ones. This makes code like this one compile

class Ref extends Mutable:
  var x = 0
  def get: Int = x
  mut def put(y: Int): Unit = x = y

class Pair[+X, +Y](val fst: X, val snd: Y)

def twoRefs1(): Pair[Ref^, Ref^] =
  Pair(Ref(), Ref())

Previously, that would not work since the X and Y variables of Pair would be instantiated to Ref^, which was not allowed.

The PR also implements lots of improvements to error diagnostics.

  • We now disambiguate different instances of ^, cap, or =>.
  • We explain for each such instances what it represents exactly.
  • We also turn this on for override errors (not necessarily limited to capture checking).

@odersky odersky force-pushed the drop-disallow-fresh branch from 223b9c6 to 99a4c20 Compare May 5, 2025 18:11
odersky added 4 commits May 6, 2025 13:34
Turn level checking on under the useFreshLevels config option (which is on by default)
Fall back to previous scheme if there is no recording in Seen. That way, we
see what we expect when turning on Ycc-verbose and other printing options.
…bles.

The check was not working properly before, which meant that problems went undetected.
To make things work again, we need two additional fixes

 - A more detailed treatment of result mapping for inferred types. In these, we map
   root.Fresh to root.Result only if the original inferred type was dependent.
   AppliedType functions are also mapped to RefinedType functions in setup, but this
   should not count for fresh to result mappings.
 - A fix in the Fresh to Result mapping of parameterless defs.
When printing types deriving from capability, do print their capture sets even if these
are implied. This avoids confusing errors where a capability classes has two different implied
Fresh instances which are both elided so the types look the same.

Exception: When printing a singleton type (x: T) we do elide in T, since the usually the only
part that matters is the `x`. But we don't do the elision under -Ycc-verbose or -explain.
@odersky odersky force-pushed the drop-disallow-fresh branch from 99a4c20 to d7eb32a Compare May 7, 2025 12:00
We had the wrong condition in maxSubsumes before. We need to test for isStaticOwner instead
of isStatic. The previous test made all fresh capabilities in global classes slip through the net.
@odersky odersky force-pushed the drop-disallow-fresh branch from d7eb32a to b58327d Compare May 8, 2025 16:24
@odersky odersky force-pushed the drop-disallow-fresh branch from b58327d to a178c04 Compare May 9, 2025 08:42
@odersky
Copy link
Contributor Author

odersky commented May 9, 2025

Most of the added lines in this PR are in the tests. We have more tests and the existing neg tests explain the problem in more detail.

@natsukagami natsukagami self-assigned this May 9, 2025
Copy link
Contributor

@natsukagami natsukagami left a comment

Choose a reason for hiding this comment

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

I also notice .check files missing for class-level-attack.scala and capt-capability.scala.

Otherwise seems good!

case _ =>
NoSymbol

final def adjustedOwner(using Context): Symbol =
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 returning the owner representing the boundary of a level? I found only one use of this method, its name is a bit mysterious

&& !yIsExistential
&& !y.isInstanceOf[TermParamRef]
&& !y.isInstanceOf[ParamRef]
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the reason for the TermParamRef -> ParamRef change? I tried changing it back and testCompilation captures still passes

@@ -183,7 +183,7 @@ sealed abstract class CaptureSet extends Showable:
*/
def accountsFor(x: CaptureRef)(using ctx: Context)(using vs: VarState = VarState.Separate): Boolean =

def debugInfo(using Context) = i"$this accountsFor $x, which has capture set ${x.captureSetOfInfo}"
def debugInfo(using Context) = i"$this accountsFor $x"
Copy link
Contributor

Choose a reason for hiding this comment

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

Intentional removal?

val argType = recheck(arg, freshenedFormal)
.showing(i"recheck arg $arg vs $freshenedFormal = $result", capt)
if formal.hasAnnotation(defn.UseAnnot) || formal.hasAnnotation(defn.ConsumeAnnot) then
// The @use and/or @consume annotation is added to `formal` by `prepareFunction`
// The @use and/or @consume annotation is added to `formal` by `prep areFunction`
Copy link
Contributor

Choose a reason for hiding this comment

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

Unintentional?

@@ -1261,8 +1277,7 @@ class CheckCaptures extends Recheck, SymTransformer:
val msg = note match
case CompareResult.LevelError(cs, ref) =>
if ref.stripReadOnly.isCapOrFresh then
def capStr = if ref.isReadOnly then "cap.rd" else "cap"
i"""the universal capability `$capStr`
i"""the universa capability $ref
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
i"""the universa capability $ref
i"""the universal capability $ref

typo

Comment on lines +873 to +880
pdenot.meet(
new JointRefDenotation(NoSymbol, rinfo, Period.allInRun(ctx.runId), pre, isRefinedMethod),
pre,
safeIntersection = ctx.base.pendingMemberSearches.contains(name))
pdenot.meet(
new JointRefDenotation(NoSymbol, rinfo, Period.allInRun(ctx.runId), pre, isRefinedMethod),
pre,
safeIntersection = ctx.base.pendingMemberSearches.contains(name))
Copy link
Contributor

Choose a reason for hiding this comment

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

pdenot.meet called twice, intentional?

Comment on lines +12 to +13
| where: => refers to the universal root capability
| cap is the universal root capability
Copy link
Contributor

Choose a reason for hiding this comment

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

This is still quite confusing I think, both are universal root capabilities

Comment on lines +18 to +20
def twoRefs3(): Pair[Ref^, Ref^] = // error but should work
val p = Pair(Ref(), Ref())
p
Copy link
Contributor

Choose a reason for hiding this comment

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

TODO?

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.

2 participants