Skip to content

Conversation

@odersky
Copy link
Contributor

@odersky odersky commented Nov 13, 2025

Based on #24352

 1. Mutable and ExclsuiveCapability are not longer classifiers. This means
    Mutable classes can capture other capabilities. Red is still a classifier.

 2. Update methods in nested classes can access exclsuive capabilities external
    to enclosing classes.
 - Allow to charge read-only set on unboxing (previously it was always the exclusive set that
   was charged).
 - Treat also methods outside Mutable types as read-only members. Previously we charged
   read-only when calling a read-only method of a mutable type, but not when calling an inherited
   method such as `eq`.
 - Use the same criterion for read-only everywhere.
 - Distinguish between read- and write-accesses to mutable fields.
…hecking

We do get sometimes capabilities like `async.rd` where `async` is shared. It
does not look straightforward to prevent that, so instead we count async.rd
as shared if async is.
We need a way to distinguish C^{} and C^{cap} when printing. We
print C^{cap} as C is C is a capability class.
This fix removed quite a few spurious error notes.
Needed to invalidate the relation

    C^{} <: C^

for Mutable types C
@odersky odersky requested a review from a team as a code owner November 13, 2025 18:19
 - New section in separation checking doc page
 - Slight generalization of expectsReadOnly
 - Lots of tests variations of scala#24373
* result of pure operation `op`, turning them into immutable types.
*/
@experimental
def immutable[T](op: -> T): T = op
Copy link
Member

@bishabosha bishabosha Nov 14, 2025

Choose a reason for hiding this comment

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

to me this sounds like a block where mutability should be prevented on the inside, (i.e. like its some pure scope). Alternatively this has an effect similar to "freeze" in other languages (i.e the result is now immutable).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I was unsure as well. I also thought of freeze before. Do you know which other languages have a construct like this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, I renamed to freeze. A search showed that it is used like this in Javascript and Haskell and it was proposed for beta.

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 mechanism in most cases is assignment to an immutable variable (const in C++, let in Swift, Rust), so not a property of the type. Javascript has Objects.freeze. .NET has a Freezable abstract class

Copy link
Member

Choose a reason for hiding this comment

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

Ruby's root Object class also has a universal freeze method

@odersky odersky changed the title Add immutable wrapper Add freeze wrapper Nov 14, 2025
* result of pure operation `op`, turning them into immutable types.
*/
@experimental
def immutable[T](op: -> T): T = op
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 not yet renamed to freeze.

@Linyxus
Copy link
Contributor

Linyxus commented Nov 14, 2025

A soundness issue related to boxes:

import language.experimental.captureChecking
import language.experimental.separationChecking
import caps.{Mutable, immutable}

// A mutable ref and its immutable version
class Ref extends Mutable:
  private var data: Int = 0
  def get: Int = data
  update def set(x: Int): Unit = data = x
def allocRef(): Ref^ = Ref()
type IRef = Ref^{}

// Boxes
case class Box[+T](unbox: T)

// Parallelism
def par(op1: () => Unit, op2: () => Unit): Unit = ()

def test1(): Unit =
  val a = allocRef()
  val xs = immutable:
    Box(a)
  val b = xs.unbox
  par(() => a.set(42), () => println(b.get))  // data race! boom

@Linyxus
Copy link
Contributor

Linyxus commented Nov 14, 2025

Another counter-example related to double-flips:

import language.experimental.captureChecking
import language.experimental.separationChecking
import caps.{Mutable, immutable}

// A mutable ref and its immutable version
class Ref extends Mutable:
  private var data: Int = 0
  def get: Int = data
  update def set(x: Int): Unit = data = x
def allocRef(): Ref^ = Ref()
type IRef = Ref^{}

def test1(): Unit =
  val magic = immutable: // should be error, but ok
    (x: Ref^) => (op: Ref^ => IRef) => op(x)
  val reallybad: Ref^ -> Ref^{} = x => magic(x)(x => x)

reallybad says that: give me any tracked Refs, I will erase the capture for you!

@odersky
Copy link
Contributor Author

odersky commented Nov 14, 2025

@Linyxus Can you try to make this work to fix the unsoundness?

@natsukagami
Copy link
Contributor

Is the first example compiling because putting a into a Box is a pure operation (doesn't charge a)?

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