Skip to content

Fixes for pattern matching under separation checking#26580

Open
odersky wants to merge 15 commits into
scala:mainfrom
dotty-staging:fix-patmat
Open

Fixes for pattern matching under separation checking#26580
odersky wants to merge 15 commits into
scala:mainfrom
dotty-staging:fix-patmat

Conversation

@odersky

@odersky odersky commented Jul 18, 2026

Copy link
Copy Markdown
Contributor
  1. Convert _i case selectors to the original case accessors. This gives more precise typing and avoids an unsoundness because _i accessor functions return pure results.
  2. Systematically propagate consume parameters into synthesized parts of case classes.
  3. Special capture check rule for case class unapply.

Based on #26569

With the changes there and in this PR we can now define recursive generic data structures of fresh elements and use these date structures in a natural way, including using pattern matching.

  • Each addition to these data structures consumes the added element.
  • Each decomposition returns fresh values and consumes the original argument.

@odersky odersky changed the title Fixes for pattern matching under cc Fixes for pattern matching under separation checking Jul 19, 2026
@odersky
odersky requested a review from natsukagami July 19, 2026 16:55

def par[A, B](x: A^, y: B^): Unit = ()

def Test =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we find a representation that permits consume tails and a unique Nil object?

Without a fresh tail, it's fairly easy to subvert the system:

def Test0 =
    val r1 = Ref()
    val r2 = Ref()
    val r3 = Ref()
    val xs0 = FreshNil
    val xs1 = FreshCons(r1, xs0)
    val xs2 = FreshCons(r2, xs1)
    val xs3 = FreshCons(r3, xs1)
    val r1_2 = xs2.tail.decons.head
    val r1_3 = xs3.tail.decons.head
    par(r1_2, r1_3) // oops

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this looks like a soundness bug if accepted. xs3.tail.decons should be allowed since it uses a consumed thing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The question is what do we expect from decons?

Right now it's

consume def decons: FreshCons[A]^ = this

Since the tail isn't marked consume I read it as
"sharing of tails is allowed".

We probably need a "deep" consume? Looking at the inferred capture sets:

val xs2: FreshCons[Ref^{}]{val tail: FreshList[Ref^{}]^{}}^{} =
          FreshCons.apply[Ref^'s2](r2, xs1)

All those lists are pure, but shouldn't we make FreshList trackable? Then I could see how decons also affects the tails.

But then we have the problem that a singleton Nil list can't be supported? I'd imagine the superclass FreshList should be an ExclusiveCapability.

@Linyxus

Linyxus commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

A minimization of the snippet posted by Oliver, which is clearly a soundness bug (double-consume):

def Test0 =
    val r1 = Ref()
    val xs0 = FreshNil
    val xs1 = FreshCons(r1, xs0)
    val ys1 = xs1.decons // ok
    val ys2 = xs1.decons // should be error, but still ok

@Linyxus

Linyxus commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

This crashes the compiler:

import language.experimental.separationChecking
import caps.*
class Ref extends Mutable:
  var x = 0
  def get: Int = x
  update def put(y: Int): Unit = x = y
trait FreshList[+A]
case class FreshCons[+A](val head: A^, val tail: FreshList[A]^) extends FreshList[A]
def Test1(xs1: FreshList[Ref^{}]^) =
  val ys = xs1 match
    case FreshCons(h, t) => FreshCons(h, t)

@odersky

odersky commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

The problem is in the type produced by FreshCons (which is a bug). If you ascribe the type explicitly, the double decons is an error:

def Test0 =
    val r1: Ref^ = Ref()
    val xs0 = FreshNil
    val xs1: FreshCons[Ref^{}]^ = FreshCons(r1, xs0)
    val ys1 = xs1.decons // ok
    val ys2 = xs1.decons // error
    par(ys1, ys2)

@odersky

odersky commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

The crash is gone now as well.

@Linyxus

Linyxus commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

another soundness issue:

import language.experimental.separationChecking
import caps.*
class Ref extends Mutable:
  var x = 0
  def get: Int = x
  update def put(y: Int): Unit = x = y
  consume def dealloc(): Unit = ()
case class FreshRef(consume val a: Ref^):
  a.dealloc()  // should be error, but ok
def boom(consume x: FreshRef^): Unit =
  x match
    case FreshRef(core) => core.dealloc()  // boom! double-free

odersky added 15 commits July 22, 2026 16:37
We already don't do that when forming CapturingTypes. We now also do not merge
capture sets if the carrier type is of the form

     (Box T^C1)^C2

There is a new variant stripOneCapturing that strips only C2 in this case.

This now supports the pos test splits.scala without resorting to the `[A <: Any^{}]` trick
that forces type variables to be shape types.
Accept unboxed -> boxed widenings without constrains. boxed -> unboxed
are the only widenings that would be unsound if we did not constrain the
capture set to be empty.
Two possibilities:

 - `hasBoxedCapsets`: contains boxed capsets (even under unboxed ones)
 - `isBoxed`: Has a boxed capset that can only be wrapped by empty capsets
 - More precise typing
 - Avoids an unsoundness because _i accessor functions return pure results.
   This causes an additional test failure in caseclass/Test_2.scala.
Propagate consume parameters in case classes to

 - apply method parameters
 - copy methods and their parameters,
 - the unapply parameter,
 - case field selectors.
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.

4 participants