From 74b6bf610e32370b70ee9d5383e304961129990b Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Wed, 18 Sep 2024 15:33:16 +0200 Subject: [PATCH 01/17] Add path support for cc --- compiler/src/dotty/tools/dotc/ast/untpd.scala | 4 +- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 3 +- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 84 +++++++++----- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 8 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 106 ++++++++++-------- .../dotty/tools/dotc/parsing/Parsers.scala | 28 ++--- .../captures/class-contra.check | 13 +-- .../captures/class-contra.scala | 3 +- .../captures/explain-under-approx.check | 14 +-- .../captures/filevar-multi-ios.scala | 41 +++++++ tests/neg-custom-args/captures/i15116.check | 16 ++- tests/neg-custom-args/captures/path-box.scala | 20 ++++ .../captures/path-connection.scala | 46 ++++++++ .../captures/path-illigal.scala | 7 ++ .../captures/path-simple.scala | 27 +++++ .../neg-custom-args/captures/singletons.scala | 8 +- .../captures/filevar-expanded.scala | 3 +- tests/pos-custom-args/captures/filevar.scala | 3 +- 18 files changed, 314 insertions(+), 120 deletions(-) create mode 100644 tests/neg-custom-args/captures/filevar-multi-ios.scala create mode 100644 tests/neg-custom-args/captures/path-box.scala create mode 100644 tests/neg-custom-args/captures/path-connection.scala create mode 100644 tests/neg-custom-args/captures/path-illigal.scala create mode 100644 tests/neg-custom-args/captures/path-simple.scala diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 60309d4d83bd..b86734afcc0a 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -525,8 +525,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { def makeRetaining(parent: Tree, refs: List[Tree], annotName: TypeName)(using Context): Annotated = Annotated(parent, New(scalaAnnotationDot(annotName), List(refs))) - def makeCapsOf(id: Ident)(using Context): Tree = - TypeApply(Select(scalaDot(nme.caps), nme.capsOf), id :: Nil) + def makeCapsOf(tp: Tree)(using Context): Tree = + TypeApply(Select(scalaDot(nme.caps), nme.capsOf), tp :: Nil) def makeCapsBound()(using Context): Tree = makeRetaining( diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 29c6528e36de..79cc7d136e45 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -194,7 +194,8 @@ extension (tp: Type) true case tp: TermRef => ((tp.prefix eq NoPrefix) - || tp.symbol.is(ParamAccessor) && tp.prefix.isThisTypeOf(tp.symbol.owner) + || tp.symbol.isField && !tp.symbol.isStatic && ( + tp.prefix.isThisTypeOf(tp.symbol.owner) || tp.prefix.isTrackableRef) || tp.isRootCapability ) && !tp.symbol.isOneOf(UnstableValueFlags) case tp: TypeRef => diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index f00c6869cd80..05162907b608 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -61,18 +61,19 @@ trait CaptureRef extends TypeProxy, ValueType: case tp: TermParamRef => tp.underlying.derivesFrom(defn.Caps_Exists) case _ => false - /** Normalize reference so that it can be compared with `eq` for equality */ - final def normalizedRef(using Context): CaptureRef = this match - case tp @ AnnotatedType(parent: CaptureRef, annot) if tp.isTrackableRef => - tp.derivedAnnotatedType(parent.normalizedRef, annot) - case tp: TermRef if tp.isTrackableRef => - tp.symbol.termRef - case _ => this + // With the support of pathes, we don't need to normalize the `TermRef`s anymore. + // /** Normalize reference so that it can be compared with `eq` for equality */ + // final def normalizedRef(using Context): CaptureRef = this match + // case tp @ AnnotatedType(parent: CaptureRef, annot) if tp.isTrackableRef => + // tp.derivedAnnotatedType(parent.normalizedRef, annot) + // case tp: TermRef if tp.isTrackableRef => + // tp.symbol.termRef + // case _ => this /** The capture set consisting of exactly this reference */ final def singletonCaptureSet(using Context): CaptureSet.Const = if mySingletonCaptureSet == null then - mySingletonCaptureSet = CaptureSet(this.normalizedRef) + mySingletonCaptureSet = CaptureSet(this) mySingletonCaptureSet.uncheckedNN /** The capture set of the type underlying this reference */ @@ -99,25 +100,56 @@ trait CaptureRef extends TypeProxy, ValueType: * x: x1.type /\ x1 subsumes y ==> x subsumes y */ final def subsumes(y: CaptureRef)(using Context): Boolean = - (this eq y) - || this.isRootCapability - || y.match - case y: TermRef => - (y.prefix eq this) - || y.info.match - case y1: SingletonCaptureRef => this.subsumes(y1) - case _ => false - case MaybeCapability(y1) => this.stripMaybe.subsumes(y1) - case _ => false - || this.match - case ReachCapability(x1) => x1.subsumes(y.stripReach) - case x: TermRef => - x.info match - case x1: SingletonCaptureRef => x1.subsumes(y) + def compareCaptureRefs(x: Type, y: Type): Boolean = + (x eq y) + || y.match + case y: CaptureRef => x.match + case x: CaptureRef => x.subsumes(y) case _ => false - case x: TermParamRef => subsumesExistentially(x, y) - case x: TypeRef => assumedContainsOf(x).contains(y) - case _ => false + case _ => false + + def compareUndelying(x: Type): Boolean = x match + case x: SingletonCaptureRef => x.subsumes(y) + case x: AndType => compareUndelying(x.tp1) || compareUndelying(x.tp2) + case x: OrType => compareUndelying(x.tp1) && compareUndelying(x.tp2) + case _ => false + + if (this eq y) || this.isRootCapability then return true + + // similar to compareNamed in TypeComparer + y match + case y: TermRef => + this match + case x: TermRef => + val xSym = x.symbol + val ySym = y.symbol + + // check x.f and y.f + if (xSym ne NoSymbol) + && (xSym eq ySym) + && compareCaptureRefs(x.prefix, y.prefix) + || (x.name eq y.name) + && x.isPrefixDependentMemberRef + && compareCaptureRefs(x.prefix, y.prefix) + && x.signature == y.signature + && !(xSym.isClass && ySym.isClass) + then return true + case _ => + + // shorten + if compareCaptureRefs(this, y.prefix) then return true + // underlying + if compareCaptureRefs(this, y.info) then return true + case MaybeCapability(y1) => return this.stripMaybe.subsumes(y1) + case _ => + + return this.match + case ReachCapability(x1) => x1.subsumes(y.stripReach) + case x: TermRef => compareUndelying(x.info) + case CapturingType(x1, _) => compareUndelying(x1) + case x: TermParamRef => subsumesExistentially(x, y) + case x: TypeRef => assumedContainsOf(x).contains(y) + case _ => false def assumedContainsOf(x: TypeRef)(using Context): SimpleIdentitySet[CaptureRef] = CaptureSet.assumedContains.getOrElse(x, SimpleIdentitySet.empty) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 44d5e2cf4b88..81b4287961ba 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -374,7 +374,7 @@ object CaptureSet: def apply(elems: CaptureRef*)(using Context): CaptureSet.Const = if elems.isEmpty then empty - else Const(SimpleIdentitySet(elems.map(_.normalizedRef.ensuring(_.isTrackableRef))*)) + else Const(SimpleIdentitySet(elems.map(_.ensuring(_.isTrackableRef))*)) def apply(elems: Refs)(using Context): CaptureSet.Const = if elems.isEmpty then empty else Const(elems) @@ -508,7 +508,11 @@ object CaptureSet: !noUniversal else elem match case elem: TermRef if level.isDefined => - elem.symbol.ccLevel <= level + elem.prefix match + case prefix: CaptureRef => + levelOK(prefix) + case _ => + elem.symbol.ccLevel <= level case elem: ThisType if level.isDefined => elem.cls.ccLevel.nextInner <= level case ReachCapability(elem1) => diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index b05ab8542137..ec1e63137311 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -122,10 +122,6 @@ object CheckCaptures: * This check is performed at Typer. */ def checkWellformed(parent: Tree, ann: Tree)(using Context): Unit = - parent.tpe match - case _: SingletonType => - report.error(em"Singleton type $parent cannot have capture set", parent.srcPos) - case _ => def check(elem: Tree, pos: SrcPos): Unit = elem.tpe match case ref: CaptureRef => if !ref.isTrackableRef then @@ -373,45 +369,54 @@ class CheckCaptures extends Recheck, SymTransformer: * the environment's owner */ def markFree(cs: CaptureSet, pos: SrcPos)(using Context): Unit = + // A captured reference with the symbol `sym` is visible from the environment + // if `sym` is not defined inside the owner of the environment. + inline def isVisibleFromEnv(sym: Symbol, env: Env) = + if env.kind == EnvKind.NestedInOwner then + !sym.isProperlyContainedIn(env.owner) + else + !sym.isContainedIn(env.owner) + + def checkSubsetEnv(cs: CaptureSet, env: Env)(using Context): Unit = + // Only captured references that are visible from the environment + // should be included. + val included = cs.filter: c => + c.stripReach match + case ref: NamedType => + val refSym = ref.symbol + val refOwner = refSym.owner + val isVisible = isVisibleFromEnv(refOwner, env) + if isVisible && !ref.isRootCapability then + ref match + case ref: TermRef if ref.prefix `ne` NoPrefix => + // If c is a path of a class defined outside the environment, + // we check the capture set of its info. + checkSubsetEnv(ref.captureSetOfInfo, env) + case _ => + if !isVisible + && (c.isReach || ref.isType) + && (!ccConfig.useSealed || refSym.is(Param)) + && refOwner == env.owner + then + if refSym.hasAnnotation(defn.UnboxAnnot) then + capt.println(i"exempt: $ref in $refOwner") + else + // Reach capabilities that go out of scope have to be approximated + // by their underlying capture set, which cannot be universal. + // Reach capabilities of @unboxed parameters are exempted. + val cs = CaptureSet.ofInfo(c) + cs.disallowRootCapability: () => + report.error(em"Local reach capability $c leaks into capture scope of ${env.ownerString}", pos) + checkSubset(cs, env.captured, pos, provenance(env)) + isVisible + case ref: ThisType => isVisibleFromEnv(ref.cls, env) + case _ => false + checkSubset(included, env.captured, pos, provenance(env)) + capt.println(i"Include call or box capture $included from $cs in ${env.owner} --> ${env.captured}") + if !cs.isAlwaysEmpty then forallOuterEnvsUpTo(ctx.owner.topLevelClass): env => - // Whether a symbol is defined inside the owner of the environment? - inline def isContainedInEnv(sym: Symbol) = - if env.kind == EnvKind.NestedInOwner then - sym.isProperlyContainedIn(env.owner) - else - sym.isContainedIn(env.owner) - // A captured reference with the symbol `sym` is visible from the environment - // if `sym` is not defined inside the owner of the environment - inline def isVisibleFromEnv(sym: Symbol) = !isContainedInEnv(sym) - // Only captured references that are visible from the environment - // should be included. - val included = cs.filter: c => - c.stripReach match - case ref: NamedType => - val refSym = ref.symbol - val refOwner = refSym.owner - val isVisible = isVisibleFromEnv(refOwner) - if !isVisible - && (c.isReach || ref.isType) - && (!ccConfig.useSealed || refSym.is(Param)) - && refOwner == env.owner - then - if refSym.hasAnnotation(defn.UnboxAnnot) then - capt.println(i"exempt: $ref in $refOwner") - else - // Reach capabilities that go out of scope have to be approximated - // by their underlying capture set, which cannot be universal. - // Reach capabilities of @unboxed parameters are exempted. - val cs = CaptureSet.ofInfo(c) - cs.disallowRootCapability: () => - report.error(em"Local reach capability $c leaks into capture scope of ${env.ownerString}", pos) - checkSubset(cs, env.captured, pos, provenance(env)) - isVisible - case ref: ThisType => isVisibleFromEnv(ref.cls) - case _ => false - checkSubset(included, env.captured, pos, provenance(env)) - capt.println(i"Include call or box capture $included from $cs in ${env.owner} --> ${env.captured}") + checkSubsetEnv(cs, env) end markFree /** Include references captured by the called method in the current environment stack */ @@ -488,21 +493,28 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => denot val selType = recheckSelection(tree, qualType, name, disambiguate) - val selCs = selType.widen.captureSet - if selCs.isAlwaysEmpty - || selType.widen.isBoxedCapturing + val selWiden = selType.widen + def isStableSel = selType match + case selType: NamedType => selType.symbol.isStableMember + case _ => false + + if pt == LhsProto || qualType.isBoxedCapturing - || pt == LhsProto + || selType.isTrackableRef + || selWiden.isBoxedCapturing + || selWiden.captureSet.isAlwaysEmpty then selType else val qualCs = qualType.captureSet - capt.println(i"pick one of $qualType, ${selType.widen}, $qualCs, $selCs in $tree") + val selCs = selType.captureSet + capt.println(i"pick one of $qualType, ${selType.widen}, $qualCs, $selCs ${selWiden.captureSet} in $tree") + if qualCs.mightSubcapture(selCs) && !selCs.mightSubcapture(qualCs) && !pt.stripCapturing.isInstanceOf[SingletonType] then - selType.widen.stripCapturing.capturing(qualCs) + selWiden.stripCapturing.capturing(qualCs) .showing(i"alternate type for select $tree: $selType --> $result, $qualCs / $selCs", capt) else selType diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 8a173faa3cec..96f09a0d6214 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1548,21 +1548,23 @@ object Parsers { case _ => None } - /** CaptureRef ::= ident [`*` | `^`] | `this` + /** CaptureRef ::= (ident | `this`) [`*` | `^`] */ def captureRef(): Tree = - if in.token == THIS then simpleRef() - else - val id = termIdent() - if isIdent(nme.raw.STAR) then - in.nextToken() - atSpan(startOffset(id)): - PostfixOp(id, Ident(nme.CC_REACH)) - else if isIdent(nme.UPARROW) then - in.nextToken() - atSpan(startOffset(id)): - makeCapsOf(cpy.Ident(id)(id.name.toTypeName)) - else id + val ref = singleton() + if isIdent(nme.raw.STAR) then + in.nextToken() + atSpan(startOffset(ref)): + PostfixOp(ref, Ident(nme.CC_REACH)) + else if isIdent(nme.UPARROW) then + in.nextToken() + def toTypeSel(r: Tree): Tree = r match + case id: Ident => cpy.Ident(id)(id.name.toTypeName) + case Select(qual, id) => Select(qual, id.toTypeName) + case _ => r + atSpan(startOffset(ref)): + makeCapsOf(toTypeSel(ref)) + else ref /** CaptureSet ::= `{` CaptureRef {`,` CaptureRef} `}` -- under captureChecking */ diff --git a/tests/neg-custom-args/captures/class-contra.check b/tests/neg-custom-args/captures/class-contra.check index 9fc009ac3d48..808118bd1795 100644 --- a/tests/neg-custom-args/captures/class-contra.check +++ b/tests/neg-custom-args/captures/class-contra.check @@ -1,10 +1,7 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/class-contra.scala:12:39 --------------------------------- -12 | def fun(x: K{val f: T^{a}}) = x.setf(a) // error - | ^ - | Found: (a : T^{x, y}) - | Required: T^{} - | - | Note that a capability (K.this.f : T^) in a capture set appearing in contravariant position - | was mapped to (x.f : T^{a}) which is not a capability. Therefore, it was under-approximated to the empty set. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/class-contra.scala:12:40 --------------------------------- +12 | def fun1(k: K{val f: T^{a}}) = k.setf(a) // error + | ^ + | Found: (a : T^{x, y}) + | Required: T^{k.f} | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/class-contra.scala b/tests/neg-custom-args/captures/class-contra.scala index 210fd4e331f1..8ef8e7485a18 100644 --- a/tests/neg-custom-args/captures/class-contra.scala +++ b/tests/neg-custom-args/captures/class-contra.scala @@ -9,5 +9,6 @@ class T def test(x: Cap, y: Cap) = val a: T^{x, y} = ??? - def fun(x: K{val f: T^{a}}) = x.setf(a) // error + def fun1(k: K{val f: T^{a}}) = k.setf(a) // error + def fun2(k: K{val f: a.type}) = k.setf(a) () \ No newline at end of file diff --git a/tests/neg-custom-args/captures/explain-under-approx.check b/tests/neg-custom-args/captures/explain-under-approx.check index 2d2b05b4b95a..c186fc6adb11 100644 --- a/tests/neg-custom-args/captures/explain-under-approx.check +++ b/tests/neg-custom-args/captures/explain-under-approx.check @@ -1,20 +1,14 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/explain-under-approx.scala:12:10 ------------------------- 12 | col.add(Future(() => 25)) // error | ^^^^^^^^^^^^^^^^ - | Found: Future[Int]{val a: (async : Async^)}^{async} - | Required: Future[Int]^{} - | - | Note that a capability Collector.this.futs* in a capture set appearing in contravariant position - | was mapped to col.futs* which is not a capability. Therefore, it was under-approximated to the empty set. + | Found: Future[Int]{val a: (async : Async^)}^{async} + | Required: Future[Int]^{col.futs*} | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/explain-under-approx.scala:15:11 ------------------------- 15 | col1.add(Future(() => 25)) // error | ^^^^^^^^^^^^^^^^ - | Found: Future[Int]{val a: (async : Async^)}^{async} - | Required: Future[Int]^{} - | - | Note that a capability Collector.this.futs* in a capture set appearing in contravariant position - | was mapped to col1.futs* which is not a capability. Therefore, it was under-approximated to the empty set. + | Found: Future[Int]{val a: (async : Async^)}^{async} + | Required: Future[Int]^{col1.futs*} | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/filevar-multi-ios.scala b/tests/neg-custom-args/captures/filevar-multi-ios.scala new file mode 100644 index 000000000000..8ffc8d8e299c --- /dev/null +++ b/tests/neg-custom-args/captures/filevar-multi-ios.scala @@ -0,0 +1,41 @@ +import language.experimental.modularity +import compiletime.uninitialized + +class IO extends caps.Capability + +class File: + def write(x: String): Unit = ??? + +object test1: + + class Service(val io: IO, val io2: IO): + var file: File^{io} = uninitialized + var file2: File^{io2} = uninitialized + def log = file.write("log") + + def withFile[T](io: IO)(op: File^{io} => T): T = + op(new File) + + def test(io3: IO, io4: IO) = + withFile(io3): f => + val o = Service(io3, io4) + o.file = f // error + o.file2 = f // error + o.log + +object test2: + + class Service(tracked val io: IO, tracked val io2: IO): + var file: File^{io} = uninitialized + var file2: File^{io2} = uninitialized + def log = file.write("log") + + def withFile[T](io: IO)(op: File^{io} => T): T = + op(new File) + + def test(io3: IO, io4: IO) = + withFile(io3): f => + val o = Service(io3, io4) + o.file = f + o.file2 = f // error + o.log diff --git a/tests/neg-custom-args/captures/i15116.check b/tests/neg-custom-args/captures/i15116.check index df05324866e1..0a16af9f6704 100644 --- a/tests/neg-custom-args/captures/i15116.check +++ b/tests/neg-custom-args/captures/i15116.check @@ -18,13 +18,17 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:5:13 ---------------------------------------- 5 | val x = Foo(m) // error | ^^^^^^ - | Found: Foo{val m: String^{Baz.this}}^{Baz.this} + | Found: Foo{val m²: (Baz.this.m : String^)}^{Baz.this.m} | Required: Foo | + | where: m is a value in trait Baz + | m² is a value in class Foo + | + | | Note that the expected type Foo | is the previously inferred type of value x | which is also the type seen in separately compiled sources. - | The new inferred type Foo{val m: String^{Baz.this}}^{Baz.this} + | The new inferred type Foo{val m: (Baz.this.m : String^)}^{Baz.this.m} | must conform to this type. | | longer explanation available when compiling with `-explain` @@ -48,13 +52,17 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:9:13 ---------------------------------------- 9 | val x = Foo(m) // error | ^^^^^^ - | Found: Foo{val m: String^{Baz2.this}}^{Baz2.this} + | Found: Foo{val m²: (Baz2.this.m : String^)}^{Baz2.this.m} | Required: Foo | + | where: m is a value in trait Baz2 + | m² is a value in class Foo + | + | | Note that the expected type Foo | is the previously inferred type of value x | which is also the type seen in separately compiled sources. - | The new inferred type Foo{val m: String^{Baz2.this}}^{Baz2.this} + | The new inferred type Foo{val m: (Baz2.this.m : String^)}^{Baz2.this.m} | must conform to this type. | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/path-box.scala b/tests/neg-custom-args/captures/path-box.scala new file mode 100644 index 000000000000..3213c236aaf5 --- /dev/null +++ b/tests/neg-custom-args/captures/path-box.scala @@ -0,0 +1,20 @@ +class A: + val m: A^ = ??? + val self: this.type = this + +case class Box[+T](value: T) + +def testBox1(a: A^): Box[A^{a}] = + Box(a.m) + +def testBox2(a: A^): Box[A^{a.m}] = + Box(a.m) + +def testBox3(a: A^): Box[A^{a.m}] = + Box(a) // error + +def testBox4(a: A^): Box[A^{a.m}] = + Box(a.m.m.m) + +def testBox5(a: A^): Box[A^{a.m}] = + Box(a.m.m.self) \ No newline at end of file diff --git a/tests/neg-custom-args/captures/path-connection.scala b/tests/neg-custom-args/captures/path-connection.scala new file mode 100644 index 000000000000..3b3820488c8d --- /dev/null +++ b/tests/neg-custom-args/captures/path-connection.scala @@ -0,0 +1,46 @@ +import language.experimental.modularity + +trait Reader: + def read(): String + +trait Sender: + def send(msg: String): Unit + +class Connection extends Reader, Sender: + def read() = "hello" + def send(msg: String) = () + + val readOnly: Reader^ = new Reader: + def read() = Connection.this.read() + +class ReaderProxy(tracked val r: Reader^) extends Reader: + def read() = "(Proxy)" + r.read() + +class SenderProxy(tracked val s: Sender^) extends Sender: + def send(msg: String) = s.send("(Proxy) " + msg) + +def testConnection(c: Connection^)( + handle1: Reader^{c.readOnly} => String, + handle2: Sender^{c} => Unit, + handle3: Reader^{c} => String, + ) = + val m1 = c.read() + c.send("hello") + + val m2 = c.readOnly.read() + + val m3a = handle1(c.readOnly) + val m3b = handle3(c.readOnly) + + val m4a = handle1(c) // error + val m4b = handle3(c) + + val m5a = handle1(new ReaderProxy(c.readOnly)) + val m5b = handle3(new ReaderProxy(c.readOnly)) + + val m6a = handle1(new ReaderProxy(c)) // error + val m6b = handle3(new ReaderProxy(c)) + + handle2(c) + + handle2(new SenderProxy(c)) \ No newline at end of file diff --git a/tests/neg-custom-args/captures/path-illigal.scala b/tests/neg-custom-args/captures/path-illigal.scala new file mode 100644 index 000000000000..f09db0087ef7 --- /dev/null +++ b/tests/neg-custom-args/captures/path-illigal.scala @@ -0,0 +1,7 @@ +class A: + val m: A^ = ??? + var n: A^ = ??? + +def test1(a: A^) = + val c1: A^{a.m} = a.m + val f1: A^{a.n} = a.n // error \ No newline at end of file diff --git a/tests/neg-custom-args/captures/path-simple.scala b/tests/neg-custom-args/captures/path-simple.scala new file mode 100644 index 000000000000..93b6dacebe74 --- /dev/null +++ b/tests/neg-custom-args/captures/path-simple.scala @@ -0,0 +1,27 @@ + +class A: + val m: A^ = ??? + val self: this.type = this + +case class C(ca: A^) + +def test1(a: A^, b: A^) = + val c1: A^{a} = a.m + val c2: A^{a.m} = a.m + val c3: A^{b} = a.m // error + + val d1: A^{a} = a.self + val d2: A^{a.self} = a.self + val d3: A^{a.self} = a + + val e1: A^{a.m} = a.self.m + val e2: A^{a.self.m} = a.self.m + val e3: A^{a.self.m} = a.m + +def test2(a: A^) = + val b: a.type = a + val c1: C^{a} = new C(a) + val c2: C^{a} = new C(a.m) + val c3: C^{a.m} = new C(a.m) + val c4: C^{b} = new C(a) + val c5: C^{a} = new C(b) \ No newline at end of file diff --git a/tests/neg-custom-args/captures/singletons.scala b/tests/neg-custom-args/captures/singletons.scala index 194e6e850dcd..be0ee67ab1bc 100644 --- a/tests/neg-custom-args/captures/singletons.scala +++ b/tests/neg-custom-args/captures/singletons.scala @@ -1,6 +1,6 @@ val x = () => () -val y1: x.type = x // ok -val y2: x.type^{} = x // error: singleton type cannot have capture set -val y3: x.type^{x} = x // error: singleton type cannot have capture set // error -val y4: x.type^ = x // error: singleton type cannot have capture set +val y1: x.type = x +val y2: x.type^{} = x +val y3: x.type^{x} = x // error +val y4: x.type^ = x diff --git a/tests/pos-custom-args/captures/filevar-expanded.scala b/tests/pos-custom-args/captures/filevar-expanded.scala index a883471e8d2e..58e7a0e67e0a 100644 --- a/tests/pos-custom-args/captures/filevar-expanded.scala +++ b/tests/pos-custom-args/captures/filevar-expanded.scala @@ -1,4 +1,5 @@ import language.experimental.captureChecking +import language.experimental.modularity import compiletime.uninitialized object test1: @@ -22,7 +23,7 @@ object test2: class File: def write(x: String): Unit = ??? - class Service(io: IO^): + class Service(tracked val io: IO^): var file: File^{io} = uninitialized def log = file.write("log") diff --git a/tests/pos-custom-args/captures/filevar.scala b/tests/pos-custom-args/captures/filevar.scala index 9ab34fe617b5..dc8d0b18908b 100644 --- a/tests/pos-custom-args/captures/filevar.scala +++ b/tests/pos-custom-args/captures/filevar.scala @@ -1,4 +1,5 @@ import language.experimental.captureChecking +import language.experimental.modularity import compiletime.uninitialized object test1: @@ -22,7 +23,7 @@ object test2: class File: def write(x: String): Unit = ??? - class Service(io: IO): + class Service(tracked val io: IO): var file: File^{io} = uninitialized def log = file.write("log") From ddba6082ba08487d528f773232ad3094e055a874 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Thu, 19 Sep 2024 15:30:57 +0200 Subject: [PATCH 02/17] Add comment for path-dependent limitation --- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 5 +---- compiler/src/dotty/tools/dotc/cc/Setup.scala | 5 +++++ tests/neg-custom-args/captures/path-connection.scala | 2 ++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index ec1e63137311..b3a1ab44d6cd 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -494,10 +494,7 @@ class CheckCaptures extends Recheck, SymTransformer: val selType = recheckSelection(tree, qualType, name, disambiguate) val selWiden = selType.widen - def isStableSel = selType match - case selType: NamedType => selType.symbol.isStableMember - case _ => false - + if pt == LhsProto || qualType.isBoxedCapturing || selType.isTrackableRef diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 22e7899eeea1..76ae41649517 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -518,6 +518,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: info match case mt: MethodOrPoly => val psyms = psymss.head + // TODO: the substitution does not work for param-dependent method types. + // For example, `(x: T, y: x.f.type) => Unit`. In this case, when we + // substitute `x.f.type`, `x` becomes a `TermParamRef`. But the new method + // type is still under initialization and `paramInfos` is still `null`, + // so the new `NamedType` will not have a denoation. mt.companion(mt.paramNames)( mt1 => if !paramSignatureChanges && !mt.isParamDependent && prevLambdas.isEmpty then diff --git a/tests/neg-custom-args/captures/path-connection.scala b/tests/neg-custom-args/captures/path-connection.scala index 3b3820488c8d..c65aa75b1ed2 100644 --- a/tests/neg-custom-args/captures/path-connection.scala +++ b/tests/neg-custom-args/captures/path-connection.scala @@ -19,6 +19,8 @@ class ReaderProxy(tracked val r: Reader^) extends Reader: class SenderProxy(tracked val s: Sender^) extends Sender: def send(msg: String) = s.send("(Proxy) " + msg) +// TODO: We have to put `c` in the different argument list to make it work. +// See the comments in `integrateRT`. def testConnection(c: Connection^)( handle1: Reader^{c.readOnly} => String, handle2: Sender^{c} => Unit, From 440c053213ae75f6fb68581cf9a3ecb4e30785e7 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 24 Sep 2024 18:05:57 +0200 Subject: [PATCH 03/17] Add alternative subsumes implementations This is done for comparing old with new --- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 46 +++++++++++++++++-- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 05162907b608..107b1a178069 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -93,23 +93,59 @@ trait CaptureRef extends TypeProxy, ValueType: final def invalidateCaches() = myCaptureSetRunId = NoRunId + final def subsumes(y: CaptureRef)(using Context): Boolean = + val was = subsumesOld(y) + val now = subsumesNew(y) + if was != now then + println(i"diff for $this subsumes $y, now: $now, ${this.getClass}, ${y.getClass}") + was + + final def subsumesOld(y: CaptureRef)(using Context): Boolean = + (this eq y) + || this.isRootCapability + || y.match + case y: TermRef => + y.prefix.match + case ypre: CaptureRef => + this.subsumesOld(ypre) + || this.match + case x @ TermRef(xpre: CaptureRef, _) => + x.symbol == y.symbol && xpre =:= ypre + case _ => + false + case _ => false + || y.info.match + case y1: SingletonCaptureRef => this.subsumesOld(y1) + case _ => false + case MaybeCapability(y1) => this.stripMaybe.subsumesOld(y1) + case _ => false + || this.match + case ReachCapability(x1) => x1.subsumesOld(y.stripReach) + case x: TermRef => + x.info match + case x1: SingletonCaptureRef => x1.subsumesOld(y) + case _ => false + case x: TermParamRef => subsumesExistentially(x, y) + case x: TypeRef => assumedContainsOf(x).contains(y) + case _ => false + /** x subsumes x * this subsumes this.f * x subsumes y ==> x* subsumes y, x subsumes y? * x subsumes y ==> x* subsumes y*, x? subsumes y? * x: x1.type /\ x1 subsumes y ==> x subsumes y */ - final def subsumes(y: CaptureRef)(using Context): Boolean = + final def subsumesNew(y: CaptureRef)(using Context): Boolean = def compareCaptureRefs(x: Type, y: Type): Boolean = (x eq y) || y.match case y: CaptureRef => x.match - case x: CaptureRef => x.subsumes(y) + case x: CaptureRef => x.subsumesNew(y) case _ => false case _ => false def compareUndelying(x: Type): Boolean = x match - case x: SingletonCaptureRef => x.subsumes(y) + case x: SingletonCaptureRef => x.subsumesNew(y) case x: AndType => compareUndelying(x.tp1) || compareUndelying(x.tp2) case x: OrType => compareUndelying(x.tp1) && compareUndelying(x.tp2) case _ => false @@ -140,11 +176,11 @@ trait CaptureRef extends TypeProxy, ValueType: if compareCaptureRefs(this, y.prefix) then return true // underlying if compareCaptureRefs(this, y.info) then return true - case MaybeCapability(y1) => return this.stripMaybe.subsumes(y1) + case MaybeCapability(y1) => return this.stripMaybe.subsumesNew(y1) case _ => return this.match - case ReachCapability(x1) => x1.subsumes(y.stripReach) + case ReachCapability(x1) => x1.subsumesNew(y.stripReach) case x: TermRef => compareUndelying(x.info) case CapturingType(x1, _) => compareUndelying(x1) case x: TermParamRef => subsumesExistentially(x, y) From 45df15269d9c237e78a4f4322a7b8e52bfec02e0 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 24 Sep 2024 18:07:50 +0200 Subject: [PATCH 04/17] Revert to previous subsumes scheme Add the path cases without changing the whole logic --- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 82 +++---------------- 1 file changed, 12 insertions(+), 70 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 107b1a178069..195f07f778eb 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -93,21 +93,21 @@ trait CaptureRef extends TypeProxy, ValueType: final def invalidateCaches() = myCaptureSetRunId = NoRunId + /** x subsumes x + * this subsumes this.f + * x subsumes y ==> x* subsumes y, x subsumes y? + * x subsumes y ==> x* subsumes y*, x? subsumes y? + * x: x1.type /\ x1 subsumes y ==> x subsumes y + * TODO: Document path cases + */ final def subsumes(y: CaptureRef)(using Context): Boolean = - val was = subsumesOld(y) - val now = subsumesNew(y) - if was != now then - println(i"diff for $this subsumes $y, now: $now, ${this.getClass}, ${y.getClass}") - was - - final def subsumesOld(y: CaptureRef)(using Context): Boolean = (this eq y) || this.isRootCapability || y.match case y: TermRef => y.prefix.match case ypre: CaptureRef => - this.subsumesOld(ypre) + this.subsumes(ypre) || this.match case x @ TermRef(xpre: CaptureRef, _) => x.symbol == y.symbol && xpre =:= ypre @@ -115,78 +115,20 @@ trait CaptureRef extends TypeProxy, ValueType: false case _ => false || y.info.match - case y1: SingletonCaptureRef => this.subsumesOld(y1) + case y1: SingletonCaptureRef => this.subsumes(y1) case _ => false - case MaybeCapability(y1) => this.stripMaybe.subsumesOld(y1) + case MaybeCapability(y1) => this.stripMaybe.subsumes(y1) case _ => false || this.match - case ReachCapability(x1) => x1.subsumesOld(y.stripReach) + case ReachCapability(x1) => x1.subsumes(y.stripReach) case x: TermRef => x.info match - case x1: SingletonCaptureRef => x1.subsumesOld(y) + case x1: SingletonCaptureRef => x1.subsumes(y) case _ => false case x: TermParamRef => subsumesExistentially(x, y) case x: TypeRef => assumedContainsOf(x).contains(y) case _ => false - /** x subsumes x - * this subsumes this.f - * x subsumes y ==> x* subsumes y, x subsumes y? - * x subsumes y ==> x* subsumes y*, x? subsumes y? - * x: x1.type /\ x1 subsumes y ==> x subsumes y - */ - final def subsumesNew(y: CaptureRef)(using Context): Boolean = - def compareCaptureRefs(x: Type, y: Type): Boolean = - (x eq y) - || y.match - case y: CaptureRef => x.match - case x: CaptureRef => x.subsumesNew(y) - case _ => false - case _ => false - - def compareUndelying(x: Type): Boolean = x match - case x: SingletonCaptureRef => x.subsumesNew(y) - case x: AndType => compareUndelying(x.tp1) || compareUndelying(x.tp2) - case x: OrType => compareUndelying(x.tp1) && compareUndelying(x.tp2) - case _ => false - - if (this eq y) || this.isRootCapability then return true - - // similar to compareNamed in TypeComparer - y match - case y: TermRef => - this match - case x: TermRef => - val xSym = x.symbol - val ySym = y.symbol - - // check x.f and y.f - if (xSym ne NoSymbol) - && (xSym eq ySym) - && compareCaptureRefs(x.prefix, y.prefix) - || (x.name eq y.name) - && x.isPrefixDependentMemberRef - && compareCaptureRefs(x.prefix, y.prefix) - && x.signature == y.signature - && !(xSym.isClass && ySym.isClass) - then return true - case _ => - - // shorten - if compareCaptureRefs(this, y.prefix) then return true - // underlying - if compareCaptureRefs(this, y.info) then return true - case MaybeCapability(y1) => return this.stripMaybe.subsumesNew(y1) - case _ => - - return this.match - case ReachCapability(x1) => x1.subsumesNew(y.stripReach) - case x: TermRef => compareUndelying(x.info) - case CapturingType(x1, _) => compareUndelying(x1) - case x: TermParamRef => subsumesExistentially(x, y) - case x: TypeRef => assumedContainsOf(x).contains(y) - case _ => false - def assumedContainsOf(x: TypeRef)(using Context): SimpleIdentitySet[CaptureRef] = CaptureSet.assumedContains.getOrElse(x, SimpleIdentitySet.empty) From 263d6eb9337f6849ee8aa77941a017d500af68ff Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 25 Sep 2024 11:57:39 +0200 Subject: [PATCH 05/17] Add logic to mark paths as used If we refer to a path `a.b`, we should mark `a.b` as used, which is better than marking `a`. --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 27 +++++++++++++------ .../dotty/tools/dotc/transform/Recheck.scala | 15 +++++------ .../dotty/tools/dotc/typer/ProtoTypes.scala | 6 +++-- tests/pos-custom-args/captures/path-use.scala | 19 +++++++++++++ 4 files changed, 49 insertions(+), 18 deletions(-) create mode 100644 tests/pos-custom-args/captures/path-use.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index b3a1ab44d6cd..05bcecf86067 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -13,7 +13,7 @@ import Trees.* import typer.RefChecks.{checkAllOverrides, checkSelfAgainstParents, OverridingPairsChecker} import typer.Checking.{checkBounds, checkAppliedTypesIn} import typer.ErrorReporting.{Addenda, NothingToAdd, err} -import typer.ProtoTypes.{AnySelectionProto, LhsProto} +import typer.ProtoTypes.{LhsProto, WildcardSelectionProto} import util.{SimpleIdentitySet, EqHashMap, EqHashSet, SrcPos, Property} import transform.{Recheck, PreRecheck, CapturedVars} import Recheck.* @@ -183,6 +183,9 @@ object CheckCaptures: /** Attachment key for bodies of closures, provided they are values */ val ClosureBodyValue = Property.Key[Unit] + /** A prototype that indicates selection with an immutable value */ + class PathSelectionProto(val sym: Symbol, val pt: Type)(using Context) extends WildcardSelectionProto + class CheckCaptures extends Recheck, SymTransformer: thisPhase => @@ -357,12 +360,13 @@ class CheckCaptures extends Recheck, SymTransformer: * the environment in which `sym` is defined. */ def markFree(sym: Symbol, pos: SrcPos)(using Context): Unit = - if sym.exists then - val ref = sym.termRef - if ref.isTracked then - forallOuterEnvsUpTo(sym.enclosure): env => - capt.println(i"Mark $sym with cs ${ref.captureSet} free in ${env.owner}") - checkElem(ref, env.captured, pos, provenance(env)) + markFree(sym, sym.termRef, pos) + + def markFree(sym: Symbol, ref: TermRef, pos: SrcPos)(using Context): Unit = + if sym.exists && ref.isTracked then + forallOuterEnvsUpTo(sym.enclosure): env => + capt.println(i"Mark $sym with cs ${ref.captureSet} free in ${env.owner}") + checkElem(ref, env.captured, pos, provenance(env)) /** Make sure (projected) `cs` is a subset of the capture sets of all enclosing * environments. At each stage, only include references from `cs` that are outside @@ -464,9 +468,16 @@ class CheckCaptures extends Recheck, SymTransformer: includeCallCaptures(tree.symbol, tree.srcPos) else //debugShowEnvs() - markFree(tree.symbol, tree.srcPos) + def addSelects(ref: TermRef, pt: Type): TermRef = pt match + case pt: PathSelectionProto => addSelects(ref.select(pt.sym).asInstanceOf[TermRef], pt.pt) + case _ => ref + markFree(tree.symbol, addSelects(tree.symbol.termRef, pt), tree.srcPos) super.recheckIdent(tree, pt) + override def selectionProto(tree: Select, pt: Type)(using Context): Type = + if !tree.symbol.isOneOf(UnstableValueFlags) then PathSelectionProto(tree.symbol, pt) + else super.selectionProto(tree, pt) + /** A specialized implementation of the selection rule. * * E |- f: T{ m: R^Cr }^{f} diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 03f0001110d3..26a75215bab1 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -12,7 +12,7 @@ import DenotTransformers.{DenotTransformer, IdentityDenotTransformer, SymTransfo import NamerOps.linkConstructorParams import NullOpsDecorator.stripNull import typer.ErrorReporting.err -import typer.ProtoTypes.* +import typer.ProtoTypes.{AnySelectionProto, LhsProto} import typer.TypeAssigner.seqLitType import typer.ConstFold import typer.ErrorReporting.{Addenda, NothingToAdd} @@ -203,13 +203,12 @@ abstract class Recheck extends Phase, SymTransformer: tree.tpe def recheckSelect(tree: Select, pt: Type)(using Context): Type = - recheckSelection(tree, recheckSelectQualifier(tree), tree.name, pt) + recheckSelection(tree, + recheck(tree.qualifier, selectionProto(tree, pt)).widenIfUnstable, + tree.name, pt) - def recheckSelectQualifier(tree: Select)(using Context): Type = - val proto = - if tree.symbol == defn.Any_asInstanceOf then WildcardType - else AnySelectionProto - recheck(tree.qualifier, proto).widenIfUnstable + def selectionProto(tree: Select, pt: Type)(using Context): Type = + if tree.symbol == defn.Any_asInstanceOf then WildcardType else AnySelectionProto def recheckSelection(tree: Select, qualType: Type, name: Name, sharpen: Denotation => Denotation)(using Context): Type = @@ -308,7 +307,7 @@ abstract class Recheck extends Phase, SymTransformer: def recheckApply(tree: Apply, pt: Type)(using Context): Type = val (funtpe0, qualType) = tree.fun match case fun: Select => - val qualType = recheckSelectQualifier(fun) + val qualType = recheck(fun.qualifier, selectionProto(fun, WildcardType)).widenIfUnstable (recheckSelection(fun, qualType, fun.name, WildcardType), qualType) case _ => (recheck(tree.fun), NoType) diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index a69a63d1ceef..53e0b456ed9a 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -324,6 +324,8 @@ object ProtoTypes { case tp: UnapplyFunProto => new UnapplySelectionProto(name, nameSpan) case tp => SelectionProto(name, IgnoredProto(tp), typer, privateOK = true, nameSpan) + class WildcardSelectionProto extends SelectionProto(nme.WILDCARD, WildcardType, NoViewsAllowed, true, NoSpan) + /** A prototype for expressions [] that are in some unspecified selection operation * * [].?: ? @@ -332,9 +334,9 @@ object ProtoTypes { * operation is further selection. In this case, the expression need not be a value. * @see checkValue */ - @sharable object AnySelectionProto extends SelectionProto(nme.WILDCARD, WildcardType, NoViewsAllowed, true, NoSpan) + @sharable object AnySelectionProto extends WildcardSelectionProto - @sharable object SingletonTypeProto extends SelectionProto(nme.WILDCARD, WildcardType, NoViewsAllowed, true, NoSpan) + @sharable object SingletonTypeProto extends WildcardSelectionProto /** A prototype for selections in pattern constructors */ class UnapplySelectionProto(name: Name, nameSpan: Span) extends SelectionProto(name, WildcardType, NoViewsAllowed, true, nameSpan) diff --git a/tests/pos-custom-args/captures/path-use.scala b/tests/pos-custom-args/captures/path-use.scala new file mode 100644 index 000000000000..5eb2b60fd218 --- /dev/null +++ b/tests/pos-custom-args/captures/path-use.scala @@ -0,0 +1,19 @@ +import language.experimental.namedTuples + +class IO + +class C(val f: IO^): + val procs: List[Proc] = ??? + +type Proc = () => Unit + +def test(io: IO^) = + val c = C(io) + val f = () => println(c.f) + val _: () ->{c.f} Unit = f + + val x = c.procs + val _: List[() ->{c.procs*} Unit] = x + + val g = () => println(c.procs.head) + val _: () ->{c.procs*} Unit = g From 152710b04a7f14a5aba57a3d828865b58bbb5855 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 25 Sep 2024 21:01:05 +0200 Subject: [PATCH 06/17] Tweaks to path checking and massage tests Needed to make stdlib2-cc go through. There were two errors. One in LayListIterable required a type annotation and a tweak to markFree. The other in Vieew.scala required a cast, but this could be fixed with better handling of pattern matching. path-patmat-should-be-pos.scala is a minimization. --- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 27 ++++++++++++------- .../dotty/tools/dotc/cc/CheckCaptures.scala | 25 ++++++++++++----- .../src/scala/collection/View.scala | 5 +++- .../immutable/LazyListIterable.scala | 4 ++- .../captures/path-patmat-should-be-pos.scala | 26 ++++++++++++++++++ 5 files changed, 69 insertions(+), 18 deletions(-) create mode 100644 tests/neg-custom-args/captures/path-patmat-should-be-pos.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 195f07f778eb..bbaf0c7d2fa0 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -101,6 +101,19 @@ trait CaptureRef extends TypeProxy, ValueType: * TODO: Document path cases */ final def subsumes(y: CaptureRef)(using Context): Boolean = + + def subsumingRefs(x: Type, y: Type): Boolean = x match + case x: CaptureRef => y match + case y: CaptureRef => x.subsumes(y) + case _ => false + case _ => false + + def viaInfo(info: Type)(test: Type => Boolean): Boolean = info.match + case info: SingletonCaptureRef => test(info) + case info: AndType => test(info.tp1) || test(info.tp2) + case info: OrType => test(info.tp1) && test(info.tp2) + case _ => false + (this eq y) || this.isRootCapability || y.match @@ -109,25 +122,21 @@ trait CaptureRef extends TypeProxy, ValueType: case ypre: CaptureRef => this.subsumes(ypre) || this.match - case x @ TermRef(xpre: CaptureRef, _) => - x.symbol == y.symbol && xpre =:= ypre + case x @ TermRef(xpre: CaptureRef, _) if x.symbol == y.symbol => + subsumingRefs(xpre, ypre) && subsumingRefs(ypre, xpre) case _ => false case _ => false - || y.info.match - case y1: SingletonCaptureRef => this.subsumes(y1) - case _ => false + || viaInfo(y.info)(subsumingRefs(this, _)) case MaybeCapability(y1) => this.stripMaybe.subsumes(y1) case _ => false || this.match case ReachCapability(x1) => x1.subsumes(y.stripReach) - case x: TermRef => - x.info match - case x1: SingletonCaptureRef => x1.subsumes(y) - case _ => false + case x: TermRef => viaInfo(x.info)(subsumingRefs(_, y)) case x: TermParamRef => subsumesExistentially(x, y) case x: TypeRef => assumedContainsOf(x).contains(y) case _ => false + end subsumes def assumedContainsOf(x: TypeRef)(using Context): SimpleIdentitySet[CaptureRef] = CaptureSet.assumedContains.getOrElse(x, SimpleIdentitySet.empty) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 05bcecf86067..19acebde8651 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -466,16 +466,24 @@ class CheckCaptures extends Recheck, SymTransformer: if tree.symbol.info.isParameterless then // there won't be an apply; need to include call captures now includeCallCaptures(tree.symbol, tree.srcPos) - else + else if !tree.symbol.isStatic then //debugShowEnvs() def addSelects(ref: TermRef, pt: Type): TermRef = pt match - case pt: PathSelectionProto => addSelects(ref.select(pt.sym).asInstanceOf[TermRef], pt.pt) + case pt: PathSelectionProto if ref.isTracked => + // if `ref` is not tracked then the selection could not give anything new + // class SerializationProxy in stdlib-cc/../LazyListIterable.scala has an example where this matters. + addSelects(ref.select(pt.sym).asInstanceOf[TermRef], pt.pt) case _ => ref - markFree(tree.symbol, addSelects(tree.symbol.termRef, pt), tree.srcPos) + val ref = tree.symbol.termRef + val pathRef = addSelects(ref, pt) + //if pathRef ne ref then + // println(i"add selects $ref --> $pathRef") + markFree(tree.symbol, if false then ref else pathRef, tree.srcPos) super.recheckIdent(tree, pt) override def selectionProto(tree: Select, pt: Type)(using Context): Type = - if !tree.symbol.isOneOf(UnstableValueFlags) then PathSelectionProto(tree.symbol, pt) + val sym = tree.symbol + if !sym.isOneOf(UnstableValueFlags) && !sym.isStatic then PathSelectionProto(sym, pt) else super.selectionProto(tree, pt) /** A specialized implementation of the selection rule. @@ -1141,11 +1149,14 @@ class CheckCaptures extends Recheck, SymTransformer: (erefs /: erefs.elems): (erefs, eref) => eref match case eref: ThisType if isPureContext(ctx.owner, eref.cls) => - erefs ++ arefs.filter { - case aref: TermRef => eref.cls.isProperlyContainedIn(aref.symbol.owner) + def isOuterRef(aref: Type): Boolean = aref match + case aref: TermRef => + val owner = aref.symbol.owner + if owner.isClass then isOuterRef(aref.prefix) + else eref.cls.isProperlyContainedIn(owner) case aref: ThisType => eref.cls.isProperlyContainedIn(aref.cls) case _ => false - } + erefs ++ arefs.filter(isOuterRef) case _ => erefs diff --git a/scala2-library-cc/src/scala/collection/View.scala b/scala2-library-cc/src/scala/collection/View.scala index 31c544a46beb..132934dbe3bd 100644 --- a/scala2-library-cc/src/scala/collection/View.scala +++ b/scala2-library-cc/src/scala/collection/View.scala @@ -150,7 +150,10 @@ object View extends IterableFactory[View] { object Filter { def apply[A](underlying: Iterable[A]^, p: A => Boolean, isFlipped: Boolean): Filter[A]^{underlying, p} = underlying match { - case filter: Filter[A] if filter.isFlipped == isFlipped => new Filter(filter.underlying, a => filter.p(a) && p(a), isFlipped) + case filter: Filter[A] if filter.isFlipped == isFlipped => + new Filter(filter.underlying, a => filter.p(a) && p(a), isFlipped) + .asInstanceOf[Filter[A]^{underlying, p}] + // !!! asInstanceOf needed once paths were added, see path-patmat-should-be-pos.scala for minimization case _ => new Filter(underlying, p, isFlipped) } } diff --git a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala index 2f7b017a6729..28ce8da104aa 100644 --- a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala +++ b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala @@ -1366,7 +1366,9 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { case SerializeEnd => initRead = true case a => init += a.asInstanceOf[A] } - val tail = in.readObject().asInstanceOf[LazyListIterable[A]] + val tail: LazyListIterable[A] = in.readObject().asInstanceOf[LazyListIterable[A]] + // Explicit type annotation needed so that tail.state below is dropped from capture set. + // Before paths were added, it was tail that was added, and the `asSeenFrom` to a pure type made it work. // scala/scala#10118: caution that no code path can evaluate `tail.state` // before the resulting LazyListIterable is returned val it = init.toList.iterator diff --git a/tests/neg-custom-args/captures/path-patmat-should-be-pos.scala b/tests/neg-custom-args/captures/path-patmat-should-be-pos.scala new file mode 100644 index 000000000000..aca6102204a3 --- /dev/null +++ b/tests/neg-custom-args/captures/path-patmat-should-be-pos.scala @@ -0,0 +1,26 @@ +class It[A] + +class Filter[A](val underlying: It[A]^, val p: A => Boolean) extends It[A] +object Filter: + def apply[A](underlying: It[A]^, p: A => Boolean): Filter[A]^{underlying, p} = + underlying match + case filter: Filter[A]^ => + val x = new Filter(filter.underlying, a => filter.p(a) && p(a)) + x: Filter[A]^{underlying, p} // error + // !!! should work, it seems to be the case that the system does not recognize that + // underlying and filter are aliases. + + // On the other hand, the following works: + locally: + val filter: underlying.type & Filter[A] = ??? + val a: It[A]^{filter.underlying} = ??? + val b: It[A]^{underlying} = a + val x = new Filter(filter.underlying, a => filter.p(a) && p(a)) + x: Filter[A]^{underlying, p} + + locally: + val filter: underlying.type & Filter[A]^ = ??? + val a: It[A]^{filter.underlying} = ??? + val b: It[A]^{underlying} = a + val x = new Filter(filter.underlying, a => filter.p(a) && p(a)) + x: Filter[A]^{underlying, p} From 215b67af405fc0b20e4af73884a2fbe0e7e352e9 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 27 Sep 2024 19:04:45 +0200 Subject: [PATCH 07/17] Account for added outer refs in the capture sets of classes --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 45 ++++++++++++++----- .../captures/outerRefsUses.scala | 10 +++++ 2 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 tests/neg-custom-args/captures/outerRefsUses.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 19acebde8651..4d905a5df4ab 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1089,7 +1089,7 @@ class CheckCaptures extends Recheck, SymTransformer: if actualBoxed eq actual then // Only `addOuterRefs` when there is no box adaptation - expected1 = addOuterRefs(expected1, actual) + expected1 = addOuterRefs(expected1, actual, tree.srcPos) if isCompatible(actualBoxed, expected1) then if debugSuccesses then tree match case Ident(_) => @@ -1130,8 +1130,12 @@ class CheckCaptures extends Recheck, SymTransformer: * that are outside `Cls`. These are all accessed through `Cls.this`, * so we can assume they are already accounted for by `Ce` and adding * them explicitly to `Ce` changes nothing. + * - To make up for this, we also add these variables to the capture set of `Cls`, + * so that all instances of `Cls` will capture these outer references. + * So in a sense we use `{Cls.this}` as a placeholder for certain outer captures. + * that we needed to be subsumed by `Cls.this`. */ - private def addOuterRefs(expected: Type, actual: Type)(using Context): Type = + private def addOuterRefs(expected: Type, actual: Type, pos: SrcPos)(using Context): Type = def isPure(info: Type): Boolean = info match case info: PolyType => isPure(info.resType) @@ -1144,19 +1148,40 @@ class CheckCaptures extends Recheck, SymTransformer: else isPure(owner.info) && isPureContext(owner.owner, limit) // Augment expeced capture set `erefs` by all references in actual capture - // set `arefs` that are outside some `this.type` reference in `erefs` + // set `arefs` that are outside some `C.this.type` reference in `erefs` for an enclosing + // class `C`. If an added reference is not a ThisType itself, add it to the capture set + // (i.e. use set) of the `C`. This makes sure that any outer reference implicitly subsumed + // by `C.this` becomes a capture reference of every instance of `C`. def augment(erefs: CaptureSet, arefs: CaptureSet): CaptureSet = (erefs /: erefs.elems): (erefs, eref) => eref match case eref: ThisType if isPureContext(ctx.owner, eref.cls) => - def isOuterRef(aref: Type): Boolean = aref match - case aref: TermRef => - val owner = aref.symbol.owner - if owner.isClass then isOuterRef(aref.prefix) - else eref.cls.isProperlyContainedIn(owner) + + def pathRoot(aref: Type): Type = aref match + case aref: NamedType if aref.symbol.owner.isClass => pathRoot(aref.prefix) + case _ => aref + + def isOuterRef(aref: Type): Boolean = pathRoot(aref) match + case aref: NamedType => eref.cls.isProperlyContainedIn(aref.symbol.owner) case aref: ThisType => eref.cls.isProperlyContainedIn(aref.cls) case _ => false - erefs ++ arefs.filter(isOuterRef) + + val outerRefs = arefs.filter(isOuterRef) + + // Include implicitly added outer references in the capture set of the class of `eref`. + for outerRef <- outerRefs.elems do + if !erefs.elems.contains(outerRef) + && !pathRoot(outerRef).isInstanceOf[ThisType] + // we don't need to add outer ThisTypes as these are anyway added as path + // prefixes at the use site. And this exemption is required since capture sets + // of non-local classes are always empty, so we can't add an outer this to them. + then + def provenance = + i""" of the enclosing class ${eref.cls}. + |The reference was included since we tried to establish that $arefs <: $erefs""" + checkElem(outerRef, capturedVars(eref.cls), pos, provenance) + + erefs ++ outerRefs case _ => erefs @@ -1341,7 +1366,7 @@ class CheckCaptures extends Recheck, SymTransformer: * @param sym symbol of the field definition that is being checked */ override def checkSubType(actual: Type, expected: Type)(using Context): Boolean = - val expected1 = alignDependentFunction(addOuterRefs(expected, actual), actual.stripCapturing) + val expected1 = alignDependentFunction(addOuterRefs(expected, actual, srcPos), actual.stripCapturing) val actual1 = val saved = curEnv try diff --git a/tests/neg-custom-args/captures/outerRefsUses.scala b/tests/neg-custom-args/captures/outerRefsUses.scala new file mode 100644 index 000000000000..cd03c8c41efd --- /dev/null +++ b/tests/neg-custom-args/captures/outerRefsUses.scala @@ -0,0 +1,10 @@ +class IO +def test(io: IO^) = + class C: + def foo() = () => + val x: IO^{this} = io + () + val c = new C + val _: C^{io} = c // ok + val _: C = c // error + () From dc8b8a0fe5c03d8eb2f9cb6f9cba66bed8cc636b Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 5 Oct 2024 17:52:19 +0200 Subject: [PATCH 08/17] Recognize double annotated capabilities such as x*? x*? is x.type @reach @maybe. This was not recognized before. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 4 ++-- tests/neg-custom-args/captures/i21646.scala | 13 ++++++++++++ tests/neg-custom-args/captures/uses.scala | 20 +++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 tests/neg-custom-args/captures/i21646.scala create mode 100644 tests/neg-custom-args/captures/uses.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 79cc7d136e45..db17022efe92 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -640,8 +640,8 @@ object CapsOfApply: class AnnotatedCapability(annot: Context ?=> ClassSymbol): def apply(tp: Type)(using Context) = AnnotatedType(tp, Annotation(annot, util.Spans.NoSpan)) - def unapply(tree: AnnotatedType)(using Context): Option[SingletonCaptureRef] = tree match - case AnnotatedType(parent: SingletonCaptureRef, ann) if ann.symbol == annot => Some(parent) + def unapply(tree: AnnotatedType)(using Context): Option[CaptureRef] = tree match + case AnnotatedType(parent: CaptureRef, ann) if ann.symbol == annot => Some(parent) case _ => None /** An extractor for `ref @annotation.internal.reachCapability`, which is used to express diff --git a/tests/neg-custom-args/captures/i21646.scala b/tests/neg-custom-args/captures/i21646.scala new file mode 100644 index 000000000000..42c493a9ea80 --- /dev/null +++ b/tests/neg-custom-args/captures/i21646.scala @@ -0,0 +1,13 @@ +import language.experimental.captureChecking +import caps.Capability + +trait File extends Capability + +class Resource[T <: Capability](gen: T): + def use[U](f: T => U): U = + f(gen) // error + +@main def run = + val myFile: File = ??? + val r = Resource(myFile) // error + () diff --git a/tests/neg-custom-args/captures/uses.scala b/tests/neg-custom-args/captures/uses.scala new file mode 100644 index 000000000000..b872c7b03ec7 --- /dev/null +++ b/tests/neg-custom-args/captures/uses.scala @@ -0,0 +1,20 @@ +class C +def test(x: C^, y: C^) = + class D { + println(x) + def foo() = println(y) + } + val d = D() + val _: D^{y} = d // error, should be ok + val _: D = d // error + + val f = () => println(D()) + val _: () ->{x} Unit = f // ok + val _: () -> Unit = f // should be error + + def g = () => + println(x) + () => println(y) + val _: () ->{x} () ->{y} Unit = g // error, should be ok + val _: () -> () -> Unit = g // error + From 1d92033a989c3bd45b036193a6b83cef3274ed69 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 11 Oct 2024 14:13:33 +0200 Subject: [PATCH 09/17] Refactor narrowCaps --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index db17022efe92..f3f1af454a04 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -469,29 +469,23 @@ extension (tp: Type) end CheckContraCaps object narrowCaps extends TypeMap: - /** Has the variance been flipped at this point? */ - private var isFlipped: Boolean = false - def apply(t: Type) = - val saved = isFlipped - try - if variance <= 0 then isFlipped = true - t.dealias match - case t1 @ CapturingType(p, cs) if cs.isUniversal && !isFlipped => - t1.derivedCapturingType(apply(p), ref.reach.singletonCaptureSet) - case t1 @ FunctionOrMethod(args, res @ Existential(_, _)) - if args.forall(_.isAlwaysPure) => - // Also map existentials in results to reach capabilities if all - // preceding arguments are known to be always pure - apply(t1.derivedFunctionOrMethod(args, Existential.toCap(res))) - case Existential(_, _) => - t - case _ => t match - case t @ CapturingType(p, cs) => - t.derivedCapturingType(apply(p), cs) // don't map capture set variables - case t => - mapOver(t) - finally isFlipped = saved + if variance <= 0 then t + else t.dealiasKeepAnnots match + case t @ CapturingType(p, cs) if cs.isUniversal => + t.derivedCapturingType(apply(p), ref.reach.singletonCaptureSet) + case t @ AnnotatedType(parent, ann) => + // Don't map annotations, which includes capture sets + t.derivedAnnotatedType(this(parent), ann) + case t @ FunctionOrMethod(args, res @ Existential(_, _)) + if args.forall(_.isAlwaysPure) => + // Also map existentials in results to reach capabilities if all + // preceding arguments are known to be always pure + apply(t.derivedFunctionOrMethod(args, Existential.toCap(res))) + case Existential(_, _) => + t + case _ => + mapOver(t) end narrowCaps ref match From d74913751e3cf4c51caef3401d2f5e6e6b42c5be Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 11 Oct 2024 15:25:15 +0200 Subject: [PATCH 10/17] Make sure dcs includes cs Previously, we violated that assumption is we too the deep capture set of a capture reference wiht singleton type. --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index f3f1af454a04..5d98856cc3b6 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -229,12 +229,12 @@ extension (tp: Type) * in the type, as computed by `CaptureSet.ofTypeDeeply`. */ def deepCaptureSet(using Context): CaptureSet = - val dcs = CaptureSet.ofTypeDeeply(tp) - if dcs.isAlwaysEmpty then dcs + val dcs = CaptureSet.ofTypeDeeply(tp.widen.stripCapturing) + if dcs.isAlwaysEmpty then tp.captureSet else tp match case tp @ ReachCapability(_) => tp.singletonCaptureSet - case tp: SingletonCaptureRef => tp.reach.singletonCaptureSet - case _ => dcs + case tp: SingletonCaptureRef if tp.isTrackableRef => tp.reach.singletonCaptureSet + case _ => tp.captureSet ++ dcs /** A type capturing `ref` */ def capturing(ref: CaptureRef)(using Context): Type = From 7693722065c042aa8cfc2d9cbfcf4834a3697c6e Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 12 Oct 2024 18:31:51 +0200 Subject: [PATCH 11/17] Align deep capture sets with reach capabilities Count in dcs exactly those locations where a cap gets replaced by a reach capability. --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 26 ++++++++++++------- tests/neg-custom-args/captures/reaches.check | 10 ++++++- tests/neg-custom-args/captures/reaches.scala | 4 +-- .../captures/refine-reach-shallow.scala | 9 ++++--- .../captures/refine-withFile.scala | 4 +-- 5 files changed, 35 insertions(+), 18 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 81b4287961ba..835e413463bd 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -1064,7 +1064,7 @@ object CaptureSet: case ref: (TermRef | TermParamRef) if ref.isMaxCapability => if ref.isTrackableRef then ref.singletonCaptureSet else CaptureSet.universal - case ReachCapability(ref1) => deepCaptureSet(ref1.widen) + case ReachCapability(ref1) => ref1.widen.deepCaptureSet .showing(i"Deep capture set of $ref: ${ref1.widen} = $result", capt) case _ => ofType(ref.underlying, followResult = true) @@ -1115,17 +1115,25 @@ object CaptureSet: /** The deep capture set of a type is the union of all covariant occurrences of * capture sets. Nested existential sets are approximated with `cap`. + * NOTE: The traversal logic needs to be in sync with narrowCaps in CaptureOps, which + * replaces caps with reach capabilties. */ def ofTypeDeeply(tp: Type)(using Context): CaptureSet = val collect = new TypeAccumulator[CaptureSet]: - def apply(cs: CaptureSet, t: Type) = t.dealias match - case t @ CapturingType(p, cs1) => - val cs2 = apply(cs, p) - if variance > 0 then cs2 ++ cs1 else cs2 - case t @ Existential(_, _) => - apply(cs, Existential.toCap(t)) - case _ => - foldOver(cs, t) + def apply(cs: CaptureSet, t: Type) = + if variance <= 0 then cs + else t.dealias match + case t @ CapturingType(p, cs1) => + this(cs, p) ++ cs1 + case t @ AnnotatedType(parent, ann) => + this(cs, parent) + case t @ FunctionOrMethod(args, res @ Existential(_, _)) + if args.forall(_.isAlwaysPure) => + this(cs, Existential.toCap(res)) + case t @ Existential(_, _) => + cs + case _ => + foldOver(cs, t) collect(CaptureSet.empty, tp) type AssumedContains = immutable.Map[TypeRef, SimpleIdentitySet[CaptureRef]] diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index f00fea09ed8c..b578934219f9 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -30,7 +30,7 @@ | ^^^^^^^^^^^^^^^^^^^ | Local reach capability id* leaks into capture scope of method test -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:62:27 -------------------------------------- -62 | val f1: File^{id*} = id(f) // error, since now id(f): File^ +62 | val f1: File^{id*} = id(f) // error, since now id(f): File^ // error | ^^^^^ | Found: File^{f} | Required: File^{id*} @@ -50,3 +50,11 @@ | Type argument () -> Unit does not conform to lower bound () => Unit | | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/reaches.scala:61:31 ----------------------------------------------------------- +61 | val leaked = usingFile[File^{id*}]: f => // error + | ^^^ + | id* cannot be tracked since its capture set is empty +-- Error: tests/neg-custom-args/captures/reaches.scala:62:18 ----------------------------------------------------------- +62 | val f1: File^{id*} = id(f) // error, since now id(f): File^ // error + | ^^^ + | id* cannot be tracked since its capture set is empty diff --git a/tests/neg-custom-args/captures/reaches.scala b/tests/neg-custom-args/captures/reaches.scala index c33ba80a668b..4db8d0df74d8 100644 --- a/tests/neg-custom-args/captures/reaches.scala +++ b/tests/neg-custom-args/captures/reaches.scala @@ -58,8 +58,8 @@ def attack2 = val id: File^ -> File^ = x => x // val id: File^ -> EX C.File^C - val leaked = usingFile[File^{id*}]: f => - val f1: File^{id*} = id(f) // error, since now id(f): File^ + val leaked = usingFile[File^{id*}]: f => // error + val f1: File^{id*} = id(f) // error, since now id(f): File^ // error f1 class List[+A]: diff --git a/tests/neg-custom-args/captures/refine-reach-shallow.scala b/tests/neg-custom-args/captures/refine-reach-shallow.scala index 525d33fdb7c5..f78c99f919af 100644 --- a/tests/neg-custom-args/captures/refine-reach-shallow.scala +++ b/tests/neg-custom-args/captures/refine-reach-shallow.scala @@ -5,14 +5,15 @@ def test1(): Unit = val g: IO^ => IO^{f*} = f // error def test2(): Unit = val f: [R] -> (IO^ => R) -> R = ??? - val g: [R] -> (IO^{f*} => R) -> R = f // error + val ff = f + val g: [R] -> (IO^{f*} => R) -> R = f // error // error def test3(): Unit = val f: [R] -> (IO^ -> R) -> R = ??? - val g: [R] -> (IO^{f*} -> R) -> R = f // error + val g: [R] -> (IO^{f*} -> R) -> R = f // error // error def test4(): Unit = val xs: List[IO^] = ??? val ys: List[IO^{xs*}] = xs // ok def test5(): Unit = val f: [R] -> (IO^ -> R) -> IO^ = ??? - val g: [R] -> (IO^ -> R) -> IO^{f*} = f // error - val h: [R] -> (IO^{f*} -> R) -> IO^ = f // error + val g: [R] -> (IO^ -> R) -> IO^{f*} = f // error // error + val h: [R] -> (IO^{f*} -> R) -> IO^ = f // error // error diff --git a/tests/neg-custom-args/captures/refine-withFile.scala b/tests/neg-custom-args/captures/refine-withFile.scala index 823b62711d05..e7958ab66fc8 100644 --- a/tests/neg-custom-args/captures/refine-withFile.scala +++ b/tests/neg-custom-args/captures/refine-withFile.scala @@ -4,5 +4,5 @@ trait File val useFile: [R] -> (path: String) -> (op: File^ -> R) -> R = ??? def main(): Unit = val f: [R] -> (path: String) -> (op: File^ -> R) -> R = useFile - val g: [R] -> (path: String) -> (op: File^{f*} -> R) -> R = f // error - val leaked = g[File^{f*}]("test")(f => f) // boom + val g: [R] -> (path: String) -> (op: File^{f*} -> R) -> R = f // error // error + val leaked = g[File^{f*}]("test")(f => f) // error From 16f86f044d601dffb9fed1bbaf28a90a13f862a2 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 13 Oct 2024 17:48:30 +0200 Subject: [PATCH 12/17] Always charge deep capture set of function arguments to cv Also: In Recheck-apply, use deep capture sets of arguments in computing the result alternative. Drop restrictions on leaking reach capabilities in markFree. Revise visibility criterion for paths. Together these changes now implement a classical capability system with reach capabilities. References that are used later after passing some arguments are already recorded in capture sets of earlier stages (exception: closure results). --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 5 + .../dotty/tools/dotc/cc/CheckCaptures.scala | 140 ++++-------------- .../dotty/tools/dotc/core/Definitions.scala | 1 - .../src/dotty/tools/dotc/core/Types.scala | 2 +- .../immutable/LazyListIterable.scala | 8 +- .../captures/cc-selftype-unsound.scala | 2 +- .../captures/effect-swaps-explicit.check | 29 ---- .../captures/effect-swaps-explicit.scala | 76 ---------- .../captures/effect-swaps.scala | 2 +- tests/neg-custom-args/captures/i16114.check | 25 ++++ tests/neg-custom-args/captures/i16114.scala | 14 +- tests/neg-custom-args/captures/i21347.check | 18 +-- tests/neg-custom-args/captures/i21347.scala | 6 +- tests/neg-custom-args/captures/i21401.check | 9 +- tests/neg-custom-args/captures/i21401.scala | 4 +- tests/neg-custom-args/captures/i21442.check | 8 - tests/neg-custom-args/captures/reaches.check | 30 ++-- tests/neg-custom-args/captures/reaches.scala | 10 +- .../captures/unbox-overrides.check | 21 --- .../captures/unbox-overrides.scala | 15 -- .../captures/unsound-reach.check | 5 - .../captures/unsound-reach.scala | 2 +- tests/neg-custom-args/captures/uses.check | 28 ++++ tests/neg-custom-args/captures/uses.scala | 8 +- .../captures/widen-reach.check | 14 +- tests/neg/leak-problem-unboxed.scala | 32 ---- tests/neg/leak-problem.scala | 19 ++- tests/pos-custom-args/captures/boxed1.scala | 2 +- .../captures}/i20503.scala | 4 +- .../captures/i21442.scala | 4 +- .../captures/list-encoding.scala | 2 +- tests/pos-custom-args/captures/path-use.scala | 4 +- .../captures/reaches-mapcompose.scala | 8 + 33 files changed, 174 insertions(+), 383 deletions(-) delete mode 100644 tests/neg-custom-args/captures/effect-swaps-explicit.check delete mode 100644 tests/neg-custom-args/captures/effect-swaps-explicit.scala create mode 100644 tests/neg-custom-args/captures/i16114.check delete mode 100644 tests/neg-custom-args/captures/i21442.check delete mode 100644 tests/neg-custom-args/captures/unbox-overrides.check delete mode 100644 tests/neg-custom-args/captures/unbox-overrides.scala create mode 100644 tests/neg-custom-args/captures/uses.check delete mode 100644 tests/neg/leak-problem-unboxed.scala rename tests/{neg => pos-custom-args/captures}/i20503.scala (87%) rename tests/{neg-custom-args => pos-custom-args}/captures/i21442.scala (78%) create mode 100644 tests/pos-custom-args/captures/reaches-mapcompose.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 5d98856cc3b6..d31afe200263 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -274,6 +274,11 @@ extension (tp: Type) case _ => tp + /** The first element of this path type */ + final def pathRoot(using Context): Type = tp.dealias match + case tp1: NamedType if tp1.symbol.owner.isClass => tp1.prefix.pathRoot + case _ => tp + /** If this is a unboxed capturing type with nonempty capture set, its boxed version. * Or, if type is a TypeBounds of capturing types, the version where the bounds are boxed. * The identity for all other types. diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 4d905a5df4ab..66add1e7d9e3 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -385,34 +385,8 @@ class CheckCaptures extends Recheck, SymTransformer: // Only captured references that are visible from the environment // should be included. val included = cs.filter: c => - c.stripReach match - case ref: NamedType => - val refSym = ref.symbol - val refOwner = refSym.owner - val isVisible = isVisibleFromEnv(refOwner, env) - if isVisible && !ref.isRootCapability then - ref match - case ref: TermRef if ref.prefix `ne` NoPrefix => - // If c is a path of a class defined outside the environment, - // we check the capture set of its info. - checkSubsetEnv(ref.captureSetOfInfo, env) - case _ => - if !isVisible - && (c.isReach || ref.isType) - && (!ccConfig.useSealed || refSym.is(Param)) - && refOwner == env.owner - then - if refSym.hasAnnotation(defn.UnboxAnnot) then - capt.println(i"exempt: $ref in $refOwner") - else - // Reach capabilities that go out of scope have to be approximated - // by their underlying capture set, which cannot be universal. - // Reach capabilities of @unboxed parameters are exempted. - val cs = CaptureSet.ofInfo(c) - cs.disallowRootCapability: () => - report.error(em"Local reach capability $c leaks into capture scope of ${env.ownerString}", pos) - checkSubset(cs, env.captured, pos, provenance(env)) - isVisible + c.stripReach.pathRoot match + case ref: NamedType => isVisibleFromEnv(ref.symbol.owner, env) case ref: ThisType => isVisibleFromEnv(ref.cls, env) case _ => false checkSubset(included, env.captured, pos, provenance(env)) @@ -424,48 +398,14 @@ class CheckCaptures extends Recheck, SymTransformer: end markFree /** Include references captured by the called method in the current environment stack */ - def includeCallCaptures(sym: Symbol, pos: SrcPos)(using Context): Unit = - if sym.exists && curEnv.isOpen then markFree(capturedVars(sym), pos) - - private val prefixCalls = util.EqHashSet[GenericApply]() - private val unboxedArgs = util.EqHashSet[Tree]() - - def handleCall(meth: Symbol, call: GenericApply, eval: () => Type)(using Context): Type = - if prefixCalls.remove(call) then return eval() - - val unboxedParamNames = - meth.rawParamss.flatMap: params => - params.collect: - case param if param.hasAnnotation(defn.UnboxAnnot) => - param.name - .toSet - - def markUnboxedArgs(call: GenericApply): Unit = call.fun.tpe.widen match - case MethodType(pnames) => - for (pname, arg) <- pnames.lazyZip(call.args) do - if unboxedParamNames.contains(pname) then - unboxedArgs.add(arg) - case _ => - - def markPrefixCalls(tree: Tree): Unit = tree match - case tree: GenericApply => - prefixCalls.add(tree) - markUnboxedArgs(tree) - markPrefixCalls(tree.fun) - case _ => - - markUnboxedArgs(call) - markPrefixCalls(call.fun) - val res = eval() - includeCallCaptures(meth, call.srcPos) - res - end handleCall + def includeCallCaptures(sym: Symbol, resType: Type, pos: SrcPos)(using Context): Unit = resType match + case _: MethodOrPoly => // wait until method is fully applied + case _ => + if sym.exists && curEnv.isOpen then markFree(capturedVars(sym), pos) override def recheckIdent(tree: Ident, pt: Type)(using Context): Type = if tree.symbol.is(Method) then - if tree.symbol.info.isParameterless then - // there won't be an apply; need to include call captures now - includeCallCaptures(tree.symbol, tree.srcPos) + includeCallCaptures(tree.symbol, tree.symbol.info, tree.srcPos) else if !tree.symbol.isStatic then //debugShowEnvs() def addSelects(ref: TermRef, pt: Type): TermRef = pt match @@ -570,15 +510,16 @@ class CheckCaptures extends Recheck, SymTransformer: tp.derivedCapturingType(forceBox(parent), refs) mapArgUsing(forceBox) else - handleCall(meth, tree, () => super.recheckApply(tree, pt)) + val res = super.recheckApply(tree, pt) + includeCallCaptures(meth, res, tree.srcPos) + res end recheckApply protected override def recheckArg(arg: Tree, formal: Type)(using Context): Type = val argType = recheck(arg, formal) - if unboxedArgs.contains(arg) then - capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}") - markFree(argType.deepCaptureSet, arg.srcPos) + capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}") + markFree(argType.deepCaptureSet, arg.srcPos) argType /** A specialized implementation of the apply rule. @@ -589,27 +530,18 @@ class CheckCaptures extends Recheck, SymTransformer: * --------------------- * E |- f(a): Tr^C * - * If the function `f` does not have an `@unboxed` parameter, then - * any unboxing it does would be charged to the environment of the function - * so they have to appear in Cq. Since any capabilities of the result of the - * application must already be present in the application, an upper - * approximation of the result capture set is Cq \union Ca, where `Ca` - * is the capture set of the argument. - * If the function `f` does have an `@unboxed` parameter, then it could in addition - * unbox reach capabilities over its formal parameter. Therefore, the approximation - * would be `Cq \union dcs(Ca)` instead. + * If the type of the function `f` does not mention any formal parameters + * any capabilities of the result of the application must already be present in + * the application. So an upper approximation of the result capture set is Cq \union Ca, + * where `Ca` is the deep capture set of the argument. * If the approximation is known to subcapture the declared result Cr, we pick it for C - * otherwise we pick Cr. + * otherwise we pick Cr. ??? */ protected override def recheckApplication(tree: Apply, qualType: Type, funType: MethodType, argTypes: List[Type])(using Context): Type = val appType = Existential.toCap(super.recheckApplication(tree, qualType, funType, argTypes)) val qualCaptures = qualType.captureSet - val argCaptures = - for (arg, argType) <- tree.args.lazyZip(argTypes) yield - if unboxedArgs.remove(arg) // need to ensure the remove happens, that's why argCaptures is computed even if not needed. - then argType.deepCaptureSet - else argType.captureSet + val argCaptures = argTypes.map(_.deepCaptureSet) appType match case appType @ CapturingType(appType1, refs) if qualType.exists @@ -704,8 +636,10 @@ class CheckCaptures extends Recheck, SymTransformer: i"Sealed type variable $pname", "be instantiated to", i"This is often caused by a local capability$where\nleaking as part of its result.", tree.srcPos) - try handleCall(meth, tree, () => Existential.toCap(super.recheckTypeApply(tree, pt))) - finally checkContains(tree) + val res = Existential.toCap(super.recheckTypeApply(tree, pt)) + includeCallCaptures(meth, res, tree.srcPos) + checkContains(tree) + res end recheckTypeApply /** Faced with a tree of form `caps.contansImpl[CS, r.type]`, check that `R` is a tracked @@ -1156,12 +1090,7 @@ class CheckCaptures extends Recheck, SymTransformer: (erefs /: erefs.elems): (erefs, eref) => eref match case eref: ThisType if isPureContext(ctx.owner, eref.cls) => - - def pathRoot(aref: Type): Type = aref match - case aref: NamedType if aref.symbol.owner.isClass => pathRoot(aref.prefix) - case _ => aref - - def isOuterRef(aref: Type): Boolean = pathRoot(aref) match + def isOuterRef(aref: Type): Boolean = aref.pathRoot match case aref: NamedType => eref.cls.isProperlyContainedIn(aref.symbol.owner) case aref: ThisType => eref.cls.isProperlyContainedIn(aref.cls) case _ => false @@ -1171,7 +1100,7 @@ class CheckCaptures extends Recheck, SymTransformer: // Include implicitly added outer references in the capture set of the class of `eref`. for outerRef <- outerRefs.elems do if !erefs.elems.contains(outerRef) - && !pathRoot(outerRef).isInstanceOf[ThisType] + && !outerRef.pathRoot.isInstanceOf[ThisType] // we don't need to add outer ThisTypes as these are anyway added as path // prefixes at the use site. And this exemption is required since capture sets // of non-local classes are always empty, so we can't add an outer this to them. @@ -1328,6 +1257,12 @@ class CheckCaptures extends Recheck, SymTransformer: /** If actual is a tracked CaptureRef `a` and widened is a capturing type T^C, * improve `T^C` to `T^{a}`, following the VAR rule of CC. + * TODO: We probably should do this also for other top-level occurrences of captures + * E.g. + * class Foo { def a: C^{io}; val def: C^{async} } + * val foo: Foo^{io, async} + * Then + * foo: Foo { def a: C^{foo}; def b: C^{foo} }^{foo} */ private def improveCaptures(widened: Type, actual: Type)(using Context): Type = actual match case ref: CaptureRef if ref.isTracked => @@ -1388,21 +1323,6 @@ class CheckCaptures extends Recheck, SymTransformer: !setup.isPreCC(overriding) && !setup.isPreCC(overridden) override def checkInheritedTraitParameters: Boolean = false - - /** Check that overrides don't change the @unbox status of their parameters */ - override def additionalChecks(member: Symbol, other: Symbol)(using Context): Unit = - for - (params1, params2) <- member.rawParamss.lazyZip(other.rawParamss) - (param1, param2) <- params1.lazyZip(params2) - do - if param1.hasAnnotation(defn.UnboxAnnot) != param2.hasAnnotation(defn.UnboxAnnot) then - report.error( - OverrideError( - i"has a parameter ${param1.name} with different @unbox status than the corresponding parameter in the overridden definition", - self, member, other, self.memberInfo(member), self.memberInfo(other) - ), - if member.owner == clazz then member.srcPos else clazz.srcPos - ) end OverridingPairsCheckerCC def traverse(t: Tree)(using Context) = diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index f95bb3cea351..68ca9f0dafc8 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1057,7 +1057,6 @@ class Definitions { @tu lazy val ExperimentalAnnot: ClassSymbol = requiredClass("scala.annotation.experimental") @tu lazy val ThrowsAnnot: ClassSymbol = requiredClass("scala.throws") @tu lazy val TransientAnnot: ClassSymbol = requiredClass("scala.transient") - @tu lazy val UnboxAnnot: ClassSymbol = requiredClass("scala.caps.unbox") @tu lazy val UncheckedAnnot: ClassSymbol = requiredClass("scala.unchecked") @tu lazy val UncheckedStableAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedStable") @tu lazy val UncheckedVarianceAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedVariance") diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index aba8c3bb31fd..60bac8221e1d 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1796,7 +1796,7 @@ object Types extends TypeUtils { /** Is this either not a method at all, or a parameterless method? */ final def isParameterless(using Context): Boolean = stripPoly match { - case mt: MethodType => false + case mt: MethodOrPoly => false case _ => true } diff --git a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala index 28ce8da104aa..ca55448991bd 100644 --- a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala +++ b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala @@ -384,7 +384,7 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz * @param suffix The collection that gets appended to this lazy list * @return The lazy list containing elements of this lazy list and the iterable object. */ - def lazyAppendedAll[B >: A](suffix: => collection.IterableOnce[B]^): LazyListIterable[B]^{this, suffix} = + def lazyAppendedAll[B >: A](suffix: => collection.IterableOnce[B]^): LazyListIterable[B]^{this, suffix*} = newLL { if (isEmpty) suffix match { case lazyList: LazyListIterable[B] => lazyList.state // don't recompute the LazyListIterable @@ -497,7 +497,7 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz * * $preservesLaziness */ - def prepended[B >: A](elem: B): LazyListIterable[B] = newLL(sCons(elem, this)) + def prepended[B >: A](elem: B): LazyListIterable[B]^{this} = newLL(sCons(elem, this)) /** @inheritdoc * @@ -1137,7 +1137,7 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { /** Construct a LazyListIterable consisting of the concatenation of the given LazyListIterable and * another LazyListIterable. */ - def #:::[B >: A](prefix: LazyListIterable[B]^): LazyListIterable[B]^{prefix, l} = prefix lazyAppendedAll l + def #:::[B >: A](prefix: LazyListIterable[B]^): LazyListIterable[B]^{prefix, l*} = prefix lazyAppendedAll l object #:: { def unapply[A](s: LazyListIterable[A]^): Option[(A, LazyListIterable[A]^{s})] = @@ -1155,7 +1155,7 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { /** Creates a State from an Iterator, with another State appended after the Iterator * is empty. */ - private def stateFromIteratorConcatSuffix[A](it: Iterator[A]^)(suffix: => State[A]^): State[A]^{it, suffix} = + private def stateFromIteratorConcatSuffix[A](it: Iterator[A]^)(suffix: => State[A]^): State[A]^{it, suffix*} = if (it.hasNext) sCons(it.next(), newLL(stateFromIteratorConcatSuffix(it)(suffix))) else suffix diff --git a/tests/neg-custom-args/captures/cc-selftype-unsound.scala b/tests/neg-custom-args/captures/cc-selftype-unsound.scala index 5aaaf5e8b8ca..07ce8c1d57d2 100644 --- a/tests/neg-custom-args/captures/cc-selftype-unsound.scala +++ b/tests/neg-custom-args/captures/cc-selftype-unsound.scala @@ -11,5 +11,5 @@ def magic(l: Logger^): Logger = Boxed[Logger^{this}](l) // error val x = new Foo val y = x.foo.unbox // y: Logger^{x} - val z: Logger = y // now the capability becomes pure + val z: Logger = y // error z diff --git a/tests/neg-custom-args/captures/effect-swaps-explicit.check b/tests/neg-custom-args/captures/effect-swaps-explicit.check deleted file mode 100644 index 264dfa663d39..000000000000 --- a/tests/neg-custom-args/captures/effect-swaps-explicit.check +++ /dev/null @@ -1,29 +0,0 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps-explicit.scala:64:8 ------------------------- -63 | Result: -64 | Future: // error, type mismatch - | ^ - | Found: Result.Ok[box Future[box T^?]^{fr, contextual$1}] - | Required: Result[Future[T], Nothing] -65 | fr.await.ok - |-------------------------------------------------------------------------------------------------------------------- - |Inline stack trace - |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |This location contains code that was inlined from effect-swaps-explicit.scala:41 -41 | boundary(Ok(body)) - | ^^^^^^^^ - -------------------------------------------------------------------------------------------------------------------- - | - | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps-explicit.scala:74:10 ------------------------ -74 | Future: fut ?=> // error: type mismatch - | ^ - | Found: Future[box T^?]^{fr, lbl} - | Required: Future[box T^?]^? -75 | fr.await.ok - | - | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/effect-swaps-explicit.scala:68:15 --------------------------------------------- -68 | Result.make: //lbl ?=> // error, escaping label from Result - | ^^^^^^^^^^^ - |local reference contextual$9 from (using contextual$9: boundary.Label[Result[box Future[box T^?]^{fr, contextual$9, contextual$9}, box E^?]]^): - | box Future[box T^?]^{fr, contextual$9, contextual$9} leaks into outer capture set of type parameter T of method make in object Result diff --git a/tests/neg-custom-args/captures/effect-swaps-explicit.scala b/tests/neg-custom-args/captures/effect-swaps-explicit.scala deleted file mode 100644 index 7474e1711b34..000000000000 --- a/tests/neg-custom-args/captures/effect-swaps-explicit.scala +++ /dev/null @@ -1,76 +0,0 @@ -//> using options -source 3.4 -// (to make sure we use the sealed policy) -object boundary: - - final class Label[-T] // extends caps.Capability - - /** Abort current computation and instead return `value` as the value of - * the enclosing `boundary` call that created `label`. - */ - def break[T](value: T)(using label: Label[T]^): Nothing = ??? - - def apply[T](body: Label[T]^ ?=> T): T = ??? -end boundary - -import boundary.{Label, break} - -trait Async extends caps.Capability -object Async: - def blocking[T](body: Async ?=> T): T = ??? - -class Future[+T]: - this: Future[T]^ => - def await(using Async): T = ??? -object Future: - def apply[T](op: Async ?=> T)(using Async): Future[T]^{op} = ??? - -enum Result[+T, +E]: - case Ok[+T](value: T) extends Result[T, Nothing] - case Err[+E](error: E) extends Result[Nothing, E] - - -object Result: - extension [T, E](r: Result[T, E]^)(using Label[Err[E]]^) - - /** `_.ok` propagates Err to current Label */ - def ok: T = r match - case Ok(value) => value - case Err(value) => break[Err[E]](Err(value)) - - transparent inline def apply[T, E](inline body: Label[Result[T, E]]^ ?=> T): Result[T, E] = - boundary(Ok(body)) - - // same as apply, but not an inline method - def make[T, E](body: Label[Result[T, E]]^ ?=> T): Result[T, E] = - boundary(Ok(body)) - -end Result - -def test[T, E](using Async) = - import Result.* - Async.blocking: async ?=> - val good1: List[Future[Result[T, E]]] => Future[Result[List[T], E]] = frs => - Future: - Result: - frs.map(_.await.ok) // OK - - val good2: Result[Future[T], E] => Future[Result[T, E]] = rf => - Future: - Result: - rf.ok.await // OK, Future argument has type Result[T] - - def fail3(fr: Future[Result[T, E]]^) = - Result: - Future: // error, type mismatch - fr.await.ok - - def fail4[T, E](fr: Future[Result[T, E]]^) = - Result.make: //lbl ?=> // error, escaping label from Result - Future: fut ?=> - fr.await.ok - - def fail5[T, E](fr: Future[Result[T, E]]^) = - Result.make[Future[T], E]: lbl ?=> - Future: fut ?=> // error: type mismatch - fr.await.ok - diff --git a/tests/neg-custom-args/captures/effect-swaps.scala b/tests/neg-custom-args/captures/effect-swaps.scala index 4bafd6421af3..13123bca7397 100644 --- a/tests/neg-custom-args/captures/effect-swaps.scala +++ b/tests/neg-custom-args/captures/effect-swaps.scala @@ -63,7 +63,7 @@ def test[T, E](using Async) = fr.await.ok def fail4[T, E](fr: Future[Result[T, E]]^) = - Result.make: // should be errorm but inders Result[Any, Any] + Result.make: // should be errorm but infers Result[Any, Any] Future: fut ?=> fr.await.ok diff --git a/tests/neg-custom-args/captures/i16114.check b/tests/neg-custom-args/captures/i16114.check new file mode 100644 index 000000000000..b4229efac303 --- /dev/null +++ b/tests/neg-custom-args/captures/i16114.check @@ -0,0 +1,25 @@ +-- Error: tests/neg-custom-args/captures/i16114.scala:18:8 ------------------------------------------------------------- +18 | fs // error + | ^^ + | (fs : Cap^) cannot be referenced here; it is not included in the allowed capture set {io} + | of an enclosing function literal with expected type Unit ->{io} Unit +-- Error: tests/neg-custom-args/captures/i16114.scala:24:8 ------------------------------------------------------------- +24 | io // error + | ^^ + | (io : Cap^) cannot be referenced here; it is not included in the allowed capture set {fs} + | of an enclosing function literal with expected type Unit ->{fs} Unit +-- Error: tests/neg-custom-args/captures/i16114.scala:34:19 ------------------------------------------------------------ +34 | expect[Cap^](io) // error + | ^^ + | reference (io : Cap^) is not included in the allowed capture set {} + | of an enclosing function literal with expected type Unit -> Unit +-- Error: tests/neg-custom-args/captures/i16114.scala:38:8 ------------------------------------------------------------- +38 | io.use() // error + | ^^ + | (io : Cap^) cannot be referenced here; it is not included in the allowed capture set {} + | of an enclosing function literal with expected type Unit -> Unit +-- Error: tests/neg-custom-args/captures/i16114.scala:39:8 ------------------------------------------------------------- +39 | io // error + | ^^ + | (io : Cap^) cannot be referenced here; it is not included in the allowed capture set {} + | of an enclosing function literal with expected type Unit -> Unit diff --git a/tests/neg-custom-args/captures/i16114.scala b/tests/neg-custom-args/captures/i16114.scala index ec04fe9c9827..0901a3d0eb4a 100644 --- a/tests/neg-custom-args/captures/i16114.scala +++ b/tests/neg-custom-args/captures/i16114.scala @@ -1,5 +1,3 @@ -//> using options -source 3.4 -// (to make sure we use the sealed policy) trait Cap { def use(): Int; def close(): Unit } def mkCap(): Cap^ = ??? @@ -15,19 +13,19 @@ def withCap[T](op: Cap^ => T): T = { def main(fs: Cap^): Unit = { def badOp(io: Cap^): Unit ->{} Unit = { val op1: Unit ->{io} Unit = (x: Unit) => - expect[Cap^] { // error + expect[Cap^] { io.use() - fs // error (limitation) + fs // error } val op2: Unit ->{fs} Unit = (x: Unit) => - expect[Cap^] { // error + expect[Cap^] { fs.use() - io // error (limitation) + io // error } val op3: Unit ->{io} Unit = (x: Unit) => - expect[Cap^] { // error + expect[Cap^] { io.use() io } @@ -36,7 +34,7 @@ def main(fs: Cap^): Unit = { expect[Cap^](io) // error val op: Unit -> Unit = (x: Unit) => - expect[Cap^] { // error + expect[Cap^] { io.use() // error io // error } diff --git a/tests/neg-custom-args/captures/i21347.check b/tests/neg-custom-args/captures/i21347.check index c680a54d3efc..fbcd67d010eb 100644 --- a/tests/neg-custom-args/captures/i21347.check +++ b/tests/neg-custom-args/captures/i21347.check @@ -1,15 +1,5 @@ --- Error: tests/neg-custom-args/captures/i21347.scala:4:15 ------------------------------------------------------------- -4 | ops.foreach: op => // error - | ^ - | Local reach capability C leaks into capture scope of method runOps -5 | op() --- Error: tests/neg-custom-args/captures/i21347.scala:8:14 ------------------------------------------------------------- +-- Error: tests/neg-custom-args/captures/i21347.scala:8:15 ------------------------------------------------------------- 8 | () => runOps(f :: Nil) // error - | ^^^^^^^^^^^^^^^^ - | reference (caps.cap : caps.Capability) is not included in the allowed capture set {} - | of an enclosing function literal with expected type () -> Unit --- Error: tests/neg-custom-args/captures/i21347.scala:11:15 ------------------------------------------------------------ -11 | ops.foreach: op => // error - | ^ - | Local reach capability ops* leaks into capture scope of method runOpsAlt -12 | op() + | ^ + | reference (f : () => Unit) is not included in the allowed capture set {} + | of an enclosing function literal with expected type () -> Unit diff --git a/tests/neg-custom-args/captures/i21347.scala b/tests/neg-custom-args/captures/i21347.scala index 41887be6a78a..ca1480612517 100644 --- a/tests/neg-custom-args/captures/i21347.scala +++ b/tests/neg-custom-args/captures/i21347.scala @@ -1,12 +1,12 @@ import language.experimental.captureChecking def runOps[C^](ops: List[() ->{C^} Unit]): Unit = - ops.foreach: op => // error + ops.foreach: op => op() def boom(f: () => Unit): () -> Unit = () => runOps(f :: Nil) // error def runOpsAlt(ops: List[() => Unit]): Unit = - ops.foreach: op => // error - op() \ No newline at end of file + ops.foreach: op => + op() diff --git a/tests/neg-custom-args/captures/i21401.check b/tests/neg-custom-args/captures/i21401.check index e204540358ce..a957c1ea11c5 100644 --- a/tests/neg-custom-args/captures/i21401.check +++ b/tests/neg-custom-args/captures/i21401.check @@ -1,3 +1,8 @@ +-- Error: tests/neg-custom-args/captures/i21401.scala:13:19 ------------------------------------------------------------ +13 | op1(Boxed[IO^](x)) // error + | ^ + | reference (x : IO^) is not included in the allowed capture set {} + | of an enclosing function literal with expected type Res -- Error: tests/neg-custom-args/captures/i21401.scala:15:22 ------------------------------------------------------------ 15 | val a = usingIO[IO^](x => x) // error: The expression's type IO^ is not allowed to capture the root capability `cap` | ^^^^^^^^^^^^^^^^^^^^ @@ -8,7 +13,3 @@ | ^^^^^^^^^^^^^^^^^^^ | The expression's type Res is not allowed to capture the root capability `cap` in its part box IO^. | This usually means that a capability persists longer than its allowed lifetime. --- Error: tests/neg-custom-args/captures/i21401.scala:18:21 ------------------------------------------------------------ -18 | val y: IO^{x*} = x.unbox // error - | ^^^^^^^ - | Local reach capability x* leaks into capture scope of method test2 diff --git a/tests/neg-custom-args/captures/i21401.scala b/tests/neg-custom-args/captures/i21401.scala index 8284c601cd5f..05fbd0757c3c 100644 --- a/tests/neg-custom-args/captures/i21401.scala +++ b/tests/neg-custom-args/captures/i21401.scala @@ -10,10 +10,10 @@ type Res = [R, X <: Boxed[IO^] -> R] -> (op: X) -> R def mkRes(x: IO^): Res = [R, X <: Boxed[IO^] -> R] => (op: X) => val op1: Boxed[IO^] -> R = op - op1(Boxed[IO^](x)) + op1(Boxed[IO^](x)) // error def test2() = val a = usingIO[IO^](x => x) // error: The expression's type IO^ is not allowed to capture the root capability `cap` val leaked: [R, X <: Boxed[IO^] -> R] -> (op: X) -> R = usingIO[Res](mkRes) // error: The expression's type Res is not allowed to capture the root capability `cap` in its part box IO^ val x: Boxed[IO^] = leaked[Boxed[IO^], Boxed[IO^] -> Boxed[IO^]](x => x) - val y: IO^{x*} = x.unbox // error + val y: IO^{x*} = x.unbox y.println("boom") diff --git a/tests/neg-custom-args/captures/i21442.check b/tests/neg-custom-args/captures/i21442.check deleted file mode 100644 index a3bbf65c5988..000000000000 --- a/tests/neg-custom-args/captures/i21442.check +++ /dev/null @@ -1,8 +0,0 @@ --- Error: tests/neg-custom-args/captures/i21442.scala:9:13 ------------------------------------------------------------- -9 | val io = x.unbox // error: local reach capability {x*} leaks - | ^^^^^^^ - | Local reach capability x* leaks into capture scope of method foo --- Error: tests/neg-custom-args/captures/i21442.scala:17:14 ------------------------------------------------------------ -17 | val io = x1.unbox // error - | ^^^^^^^^ - | Local reach capability x1* leaks into capture scope of method bar diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index b578934219f9..663fe26664bd 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -25,10 +25,18 @@ | ^^^^^^^^^^^^ | The expression's type box () => Unit is not allowed to capture the root capability `cap`. | This usually means that a capability persists longer than its allowed lifetime. --- Error: tests/neg-custom-args/captures/reaches.scala:55:6 ------------------------------------------------------------ -55 | id(() => f.write()) // error - | ^^^^^^^^^^^^^^^^^^^ - | Local reach capability id* leaks into capture scope of method test +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:53:2 --------------------------------------- +53 | val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error + | ^ + | Found: box () => Unit + | Required: () => Unit + | + | Note that box () => Unit cannot be box-converted to () => Unit + | since at least one of their capture sets contains the root capability `cap` +54 | usingFile: f => +55 | id(() => f.write()) + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:62:27 -------------------------------------- 62 | val f1: File^{id*} = id(f) // error, since now id(f): File^ // error | ^^^^^ @@ -36,20 +44,6 @@ | Required: File^{id*} | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/reaches.scala:79:10 ----------------------------------------------------------- -79 | ps.map((x, y) => compose1(x, y)) // error // error - | ^ - | Local reach capability ps* leaks into capture scope of method mapCompose --- Error: tests/neg-custom-args/captures/reaches.scala:79:13 ----------------------------------------------------------- -79 | ps.map((x, y) => compose1(x, y)) // error // error - | ^ - | Local reach capability ps* leaks into capture scope of method mapCompose --- [E057] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:53:51 -------------------------------------- -53 | val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error - | ^ - | Type argument () -> Unit does not conform to lower bound () => Unit - | - | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/reaches.scala:61:31 ----------------------------------------------------------- 61 | val leaked = usingFile[File^{id*}]: f => // error | ^^^ diff --git a/tests/neg-custom-args/captures/reaches.scala b/tests/neg-custom-args/captures/reaches.scala index 4db8d0df74d8..50731fbd0fb2 100644 --- a/tests/neg-custom-args/captures/reaches.scala +++ b/tests/neg-custom-args/captures/reaches.scala @@ -52,7 +52,7 @@ class Id[-A, +B >: A](): def test = val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error usingFile: f => - id(() => f.write()) // error + id(() => f.write()) def attack2 = val id: File^ -> File^ = x => x @@ -72,11 +72,3 @@ extension [A](x: A) def :: (xs: List[A]): List[A] = ??? object Nil extends List[Nothing] -def compose1[A, B, C](f: A => B, g: B => C): A ->{f, g} C = - z => g(f(z)) - -def mapCompose[A](ps: List[(A => A, A => A)]): List[A ->{ps*} A] = - ps.map((x, y) => compose1(x, y)) // error // error - -def mapCompose2[A](@unbox ps: List[(A => A, A => A)]): List[A ->{ps*} A] = - ps.map((x, y) => compose1(x, y)) diff --git a/tests/neg-custom-args/captures/unbox-overrides.check b/tests/neg-custom-args/captures/unbox-overrides.check deleted file mode 100644 index b9a3be7bffbc..000000000000 --- a/tests/neg-custom-args/captures/unbox-overrides.check +++ /dev/null @@ -1,21 +0,0 @@ --- [E164] Declaration Error: tests/neg-custom-args/captures/unbox-overrides.scala:8:6 ---------------------------------- -8 | def foo(x: C): C // error - | ^ - |error overriding method foo in trait A of type (x: C): C; - | method foo of type (x: C): C has a parameter x with different @unbox status than the corresponding parameter in the overridden definition - | - | longer explanation available when compiling with `-explain` --- [E164] Declaration Error: tests/neg-custom-args/captures/unbox-overrides.scala:9:6 ---------------------------------- -9 | def bar(@unbox x: C): C // error - | ^ - |error overriding method bar in trait A of type (x: C): C; - | method bar of type (x: C): C has a parameter x with different @unbox status than the corresponding parameter in the overridden definition - | - | longer explanation available when compiling with `-explain` --- [E164] Declaration Error: tests/neg-custom-args/captures/unbox-overrides.scala:15:15 -------------------------------- -15 |abstract class C extends A[C], B2 // error - | ^ - |error overriding method foo in trait A of type (x: C): C; - | method foo in trait B2 of type (x: C): C has a parameter x with different @unbox status than the corresponding parameter in the overridden definition - | - | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/unbox-overrides.scala b/tests/neg-custom-args/captures/unbox-overrides.scala deleted file mode 100644 index 5abb5013bfbe..000000000000 --- a/tests/neg-custom-args/captures/unbox-overrides.scala +++ /dev/null @@ -1,15 +0,0 @@ -import caps.unbox - -trait A[X]: - def foo(@unbox x: X): X - def bar(x: X): X - -trait B extends A[C]: - def foo(x: C): C // error - def bar(@unbox x: C): C // error - -trait B2: - def foo(x: C): C - def bar(@unbox x: C): C - -abstract class C extends A[C], B2 // error diff --git a/tests/neg-custom-args/captures/unsound-reach.check b/tests/neg-custom-args/captures/unsound-reach.check index 4a6793d204c5..f0e4c4deeb41 100644 --- a/tests/neg-custom-args/captures/unsound-reach.check +++ b/tests/neg-custom-args/captures/unsound-reach.check @@ -1,8 +1,3 @@ --- Error: tests/neg-custom-args/captures/unsound-reach.scala:18:21 ----------------------------------------------------- -18 | boom.use(f): (f1: File^{backdoor*}) => // error - | ^ - | Local reach capability backdoor* leaks into capture scope of method bad -19 | escaped = f1 -- [E164] Declaration Error: tests/neg-custom-args/captures/unsound-reach.scala:10:8 ----------------------------------- 10 | def use(x: File^)(op: File^ => Unit): Unit = op(x) // error, was OK using sealed checking | ^ diff --git a/tests/neg-custom-args/captures/unsound-reach.scala b/tests/neg-custom-args/captures/unsound-reach.scala index c3c31a7f32ff..2dcea24ff0f0 100644 --- a/tests/neg-custom-args/captures/unsound-reach.scala +++ b/tests/neg-custom-args/captures/unsound-reach.scala @@ -15,6 +15,6 @@ def bad(): Unit = var escaped: File^{backdoor*} = null withFile("hello.txt"): f => - boom.use(f): (f1: File^{backdoor*}) => // error + boom.use(f): (f1: File^{backdoor*}) => escaped = f1 diff --git a/tests/neg-custom-args/captures/uses.check b/tests/neg-custom-args/captures/uses.check new file mode 100644 index 000000000000..d8c41692f2cc --- /dev/null +++ b/tests/neg-custom-args/captures/uses.check @@ -0,0 +1,28 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/uses.scala:8:17 ------------------------------------------ +8 | val _: D^{y} = d // error + | ^ + | Found: (d : D^{x, y}) + | Required: D^{y} + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/uses.scala:9:13 ------------------------------------------ +9 | val _: D = d // error + | ^ + | Found: (d : D^{x, y}) + | Required: D + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/uses.scala:13:22 ----------------------------------------- +13 | val _: () -> Unit = f // error + | ^ + | Found: (f : () ->{x, y} Unit) + | Required: () -> Unit + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/uses.scala:19:28 ----------------------------------------- +19 | val _: () -> () -> Unit = g // error + | ^ + | Found: () ->{x, y} (ex$7: caps.Exists) -> () ->{y} Unit + | Required: () -> () -> Unit + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/uses.scala b/tests/neg-custom-args/captures/uses.scala index b872c7b03ec7..c61d4baf2184 100644 --- a/tests/neg-custom-args/captures/uses.scala +++ b/tests/neg-custom-args/captures/uses.scala @@ -5,16 +5,16 @@ def test(x: C^, y: C^) = def foo() = println(y) } val d = D() - val _: D^{y} = d // error, should be ok + val _: D^{y} = d // error val _: D = d // error val f = () => println(D()) - val _: () ->{x} Unit = f // ok - val _: () -> Unit = f // should be error + val _: () ->{x, y} Unit = f // ok + val _: () -> Unit = f // error def g = () => println(x) () => println(y) - val _: () ->{x} () ->{y} Unit = g // error, should be ok + val _: () ->{x, y} () ->{y} Unit = g val _: () -> () -> Unit = g // error diff --git a/tests/neg-custom-args/captures/widen-reach.check b/tests/neg-custom-args/captures/widen-reach.check index 06d21ff445d8..dbe811ab99ec 100644 --- a/tests/neg-custom-args/captures/widen-reach.check +++ b/tests/neg-custom-args/captures/widen-reach.check @@ -1,11 +1,17 @@ --- Error: tests/neg-custom-args/captures/widen-reach.scala:13:26 ------------------------------------------------------- +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/widen-reach.scala:13:26 ---------------------------------- 13 | val y2: IO^ -> IO^ = y1.foo // error | ^^^^^^ - | Local reach capability x* leaks into capture scope of method test --- Error: tests/neg-custom-args/captures/widen-reach.scala:14:30 ------------------------------------------------------- + | Found: IO^ ->{x*} IO^{x*} + | Required: IO^ -> (ex$6: caps.Exists) -> IO^{ex$6} + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/widen-reach.scala:14:30 ---------------------------------- 14 | val y3: IO^ -> IO^{x*} = y1.foo // error | ^^^^^^ - | Local reach capability x* leaks into capture scope of method test + | Found: IO^ ->{x*} IO^{x*} + | Required: IO^ -> IO^{x*} + | + | longer explanation available when compiling with `-explain` -- [E164] Declaration Error: tests/neg-custom-args/captures/widen-reach.scala:9:6 -------------------------------------- 9 | val foo: IO^ -> IO^ = x => x // error | ^ diff --git a/tests/neg/leak-problem-unboxed.scala b/tests/neg/leak-problem-unboxed.scala deleted file mode 100644 index 7de3d84bfcca..000000000000 --- a/tests/neg/leak-problem-unboxed.scala +++ /dev/null @@ -1,32 +0,0 @@ -import language.experimental.captureChecking -import caps.unbox - -// Some capabilities that should be used locally -trait Async: - // some method - def read(): Unit -def usingAsync[X](op: Async^ => X): X = ??? - -case class Box[+T](get: T) - -def useBoxedAsync(@unbox x: Box[Async^]): Unit = - val t0 = x - val t1 = t0.get // ok - t1.read() - -def useBoxedAsync1(@unbox x: Box[Async^]): Unit = x.get.read() // ok - -def test(): Unit = - - val f: Box[Async^] => Unit = (x: Box[Async^]) => useBoxedAsync(x) // error - val _: Box[Async^] => Unit = useBoxedAsync(_) // error - val _: Box[Async^] => Unit = useBoxedAsync // error - val _ = useBoxedAsync(_) // error - val _ = useBoxedAsync // error - - def boom(x: Async^): () ->{f} Unit = - () => f(Box(x)) - - val leaked = usingAsync[() ->{f} Unit](boom) - - leaked() // scope violation \ No newline at end of file diff --git a/tests/neg/leak-problem.scala b/tests/neg/leak-problem.scala index 354d54d86707..c271e5d02560 100644 --- a/tests/neg/leak-problem.scala +++ b/tests/neg/leak-problem.scala @@ -10,21 +10,32 @@ case class Box[+T](get: T) def useBoxedAsync(x: Box[Async^]): Unit = val t0 = x - val t1 = t0.get // error + val t1 = t0.get // now ok t1.read() -def useBoxedAsync1(x: Box[Async^]): Unit = x.get.read() // error +def useBoxedAsync1(x: Box[Async^]): Unit = x.get.read() // now ok def test(): Unit = + val xs: Box[Async^] = ??? + val xsLambda = () => useBoxedAsync(xs) + val _: () ->{xs*} Unit = xsLambda + val _: () -> Unit = xsLambda // error + val useBoxedAsync2 = (x: Box[Async^]) => val t0 = x - val t1 = x.get // error + val t1 = x.get t1.read() + val xsLambda2 = () => useBoxedAsync2(xs) + val _: () ->{xs*} Unit = xsLambda + val _: () -> Unit = xsLambda // error + val f: Box[Async^] => Unit = (x: Box[Async^]) => useBoxedAsync(x) def boom(x: Async^): () ->{f} Unit = - () => f(Box(x)) + val ff = () => f(Box(x)) // error + val _: () ->{f, x} Unit = ff + ff val leaked = usingAsync[() ->{f} Unit](boom) diff --git a/tests/pos-custom-args/captures/boxed1.scala b/tests/pos-custom-args/captures/boxed1.scala index e2ff69c305d2..0aceadab6c81 100644 --- a/tests/pos-custom-args/captures/boxed1.scala +++ b/tests/pos-custom-args/captures/boxed1.scala @@ -8,4 +8,4 @@ def test(c: Cap) = val f = () => { c; 1 } val _: () ->{c} Int = f val g = () => Box(f) - val _: () -> Box[() ->{f} Int] = g + val _: () ->{g} Box[() ->{f} Int] = g diff --git a/tests/neg/i20503.scala b/tests/pos-custom-args/captures/i20503.scala similarity index 87% rename from tests/neg/i20503.scala rename to tests/pos-custom-args/captures/i20503.scala index 3fb0573f6c2f..5fb013391b11 100644 --- a/tests/neg/i20503.scala +++ b/tests/pos-custom-args/captures/i20503.scala @@ -14,5 +14,5 @@ def runOps(@unbox ops: List[() => Unit]): Unit = ops.foreach(op => op()) def main(): Unit = - val f: List[() => Unit] -> Unit = (ops: List[() => Unit]) => runOps(ops) // error - val _: List[() => Unit] -> Unit = runOps // error + val f: List[() => Unit] -> Unit = (ops: List[() => Unit]) => runOps(ops) // now ok + val _: List[() => Unit] -> Unit = runOps // now ok diff --git a/tests/neg-custom-args/captures/i21442.scala b/tests/pos-custom-args/captures/i21442.scala similarity index 78% rename from tests/neg-custom-args/captures/i21442.scala rename to tests/pos-custom-args/captures/i21442.scala index c9fa7d152fae..fbc3d57ee333 100644 --- a/tests/neg-custom-args/captures/i21442.scala +++ b/tests/pos-custom-args/captures/i21442.scala @@ -6,7 +6,7 @@ case class Boxed[+T](unbox: T) // `foo` is a function that unboxes its parameter // and uses the capability boxed inside the parameter. def foo(x: Boxed[IO^]): Unit = - val io = x.unbox // error: local reach capability {x*} leaks + val io = x.unbox // now ok, was error: local reach capability {x*} leaks io.use() // `bar` is a function that does the same thing in a @@ -14,5 +14,5 @@ def foo(x: Boxed[IO^]): Unit = // But, no type error reported. def bar(x: Boxed[IO^]): Unit = val x1: Boxed[IO^] = x - val io = x1.unbox // error + val io = x1.unbox // now ok, was error io.use() diff --git a/tests/pos-custom-args/captures/list-encoding.scala b/tests/pos-custom-args/captures/list-encoding.scala index d959b523404b..8e94021c1285 100644 --- a/tests/pos-custom-args/captures/list-encoding.scala +++ b/tests/pos-custom-args/captures/list-encoding.scala @@ -7,7 +7,7 @@ type Op[T, C] = (v: T) => (s: C) => C type List[T] = - [C] -> (op: Op[T, C]) -> (s: C) ->{op} C + [C] -> (op: Op[T, C]) -> (s: C) ->{op*} C def nil[T]: List[T] = [C] => (op: Op[T, C]) => (s: C) => s diff --git a/tests/pos-custom-args/captures/path-use.scala b/tests/pos-custom-args/captures/path-use.scala index 5eb2b60fd218..821bfc8ced21 100644 --- a/tests/pos-custom-args/captures/path-use.scala +++ b/tests/pos-custom-args/captures/path-use.scala @@ -9,8 +9,8 @@ type Proc = () => Unit def test(io: IO^) = val c = C(io) - val f = () => println(c.f) - val _: () ->{c.f} Unit = f + val ff = () => println(c.f) + val _: () ->{c.f} Unit = ff val x = c.procs val _: List[() ->{c.procs*} Unit] = x diff --git a/tests/pos-custom-args/captures/reaches-mapcompose.scala b/tests/pos-custom-args/captures/reaches-mapcompose.scala new file mode 100644 index 000000000000..a01a8d30e67f --- /dev/null +++ b/tests/pos-custom-args/captures/reaches-mapcompose.scala @@ -0,0 +1,8 @@ +def compose1[A, B, C](f: A => B, g: B => C): A ->{f, g} C = + z => g(f(z)) + +def mapCompose[A](ps: List[(A => A, A => A)]): List[A ->{ps*} A] = + ps.map((x, y) => compose1(x, y)) // now ok + +def mapCompose2[A](ps: List[(A => A, A => A)]): List[A ->{ps*} A] = + ps.map((x, y) => compose1(x, y)) From 1836fb3ec40cf1b64cfba47c5b76983cd669717e Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 14 Oct 2024 15:43:49 +0200 Subject: [PATCH 13/17] Drop `@unbox` annotation --- library/src/scala/caps.scala | 5 --- tests/neg-custom-args/captures/i15749a.scala | 3 +- tests/neg-custom-args/captures/i21614.check | 8 ++--- tests/neg-custom-args/captures/i21614.scala | 5 ++- .../captures/leak-problem-2.scala | 2 +- tests/neg-custom-args/captures/reaches.check | 36 +++++++++---------- tests/neg-custom-args/captures/reaches.scala | 5 ++- .../captures/spread-problem.scala | 2 +- .../pos-custom-args/captures/dep-reach.scala | 5 ++- tests/pos-custom-args/captures/i20503.scala | 3 +- tests/pos-custom-args/captures/reaches.scala | 3 +- tests/pos/cc-poly-source-capability.scala | 3 +- tests/pos/cc-poly-source.scala | 3 +- tests/pos/gears-probem-1.scala | 3 +- tests/pos/i13541.scala | 2 +- tests/pos/i18699.scala | 3 +- tests/pos/i20342.scala | 4 +-- tests/pos/reach-capability.scala | 3 +- tests/pos/reach-problem.scala | 3 +- 19 files changed, 42 insertions(+), 59 deletions(-) diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index 9911ef920116..b56724422b25 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -52,11 +52,6 @@ import annotation.{experimental, compileTimeOnly, retainsCap} */ final class untrackedCaptures extends annotation.StaticAnnotation - /** This should go into annotations. For now it is here, so that we - * can experiment with it quickly between minor releases - */ - final class unbox extends annotation.StaticAnnotation - object unsafe: extension [T](x: T) diff --git a/tests/neg-custom-args/captures/i15749a.scala b/tests/neg-custom-args/captures/i15749a.scala index 57fca27fae66..bba69fb239a8 100644 --- a/tests/neg-custom-args/captures/i15749a.scala +++ b/tests/neg-custom-args/captures/i15749a.scala @@ -1,5 +1,4 @@ import caps.cap -import caps.unbox class Unit object u extends Unit @@ -18,7 +17,7 @@ def test = def force[A](thunk: Unit ->{cap} A): A = thunk(u) - def forceWrapper[A](@unbox mx: Wrapper[Unit ->{cap} A]): Wrapper[A] = + def forceWrapper[A](mx: Wrapper[Unit ->{cap} A]): Wrapper[A] = // Γ ⊢ mx: Wrapper[□ {cap} Unit => A] // `force` should be typed as ∀(□ {cap} Unit -> A) A, but it can not strictMap[Unit ->{mx*} A, A](mx)(t => force[A](t)) // error // should work diff --git a/tests/neg-custom-args/captures/i21614.check b/tests/neg-custom-args/captures/i21614.check index 14b468db4c8e..14e54c50ae04 100644 --- a/tests/neg-custom-args/captures/i21614.check +++ b/tests/neg-custom-args/captures/i21614.check @@ -1,12 +1,12 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:9:33 ---------------------------------------- -9 | files.map((f: F) => new Logger(f)) // error, Q: can we make this pass (see #19076)? +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:8:33 ---------------------------------------- +8 | files.map((f: F) => new Logger(f)) // error, Q: can we make this pass (see #19076)? | ^ | Found: (f : F^) | Required: File^ | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:12:12 --------------------------------------- -12 | files.map(new Logger(_)) // error, Q: can we improve the error message? +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:11:12 --------------------------------------- +11 | files.map(new Logger(_)) // error, Q: can we improve the error message? | ^^^^^^^^^^^^^ | Found: Logger{val f: (_$1 : File^{files*})}^ | Required: Logger{val f: File^?}^? diff --git a/tests/neg-custom-args/captures/i21614.scala b/tests/neg-custom-args/captures/i21614.scala index a5ed25d818a5..f1b0f43cc328 100644 --- a/tests/neg-custom-args/captures/i21614.scala +++ b/tests/neg-custom-args/captures/i21614.scala @@ -1,12 +1,11 @@ import language.experimental.captureChecking import caps.Capability -import caps.unbox trait File extends Capability class Logger(f: File^) extends Capability // <- will work if we remove the extends clause -def mkLoggers1[F <: File^](@unbox files: List[F]): List[Logger^] = +def mkLoggers1[F <: File^](files: List[F]): List[Logger^] = files.map((f: F) => new Logger(f)) // error, Q: can we make this pass (see #19076)? -def mkLoggers2(@unbox files: List[File^]): List[Logger^] = +def mkLoggers2(files: List[File^]): List[Logger^] = files.map(new Logger(_)) // error, Q: can we improve the error message? diff --git a/tests/neg-custom-args/captures/leak-problem-2.scala b/tests/neg-custom-args/captures/leak-problem-2.scala index 08a3a6c2d9ca..3b6ef182997e 100644 --- a/tests/neg-custom-args/captures/leak-problem-2.scala +++ b/tests/neg-custom-args/captures/leak-problem-2.scala @@ -2,7 +2,7 @@ import language.experimental.captureChecking trait Source[+T] -def race[T](@caps.unbox sources: Seq[Source[T]^]): Source[T]^{sources*} = ??? +def race[T](sources: Seq[Source[T]^]): Source[T]^{sources*} = ??? def raceTwo[T](src1: Source[T]^, src2: Source[T]^): Source[T]^{} = race(Seq(src1, src2)) // error diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index 663fe26664bd..d5facb0f8529 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -1,12 +1,12 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:22:11 -------------------------------------- -22 | cur = (() => f.write()) :: Nil // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:21:11 -------------------------------------- +21 | cur = (() => f.write()) :: Nil // error | ^^^^^^^^^^^^^^^^^^^^^^^ | Found: List[box () ->{f} Unit] | Required: List[box () ->{xs*} Unit] | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:33:7 --------------------------------------- -33 | (() => f.write()) :: Nil // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:32:7 --------------------------------------- +32 | (() => f.write()) :: Nil // error | ^^^^^^^^^^^^^^^^^^^^^^^ | Found: List[box () ->{f} Unit] | Required: box List[box () ->{xs*} Unit]^? @@ -15,40 +15,40 @@ | cannot be included in outer capture set {xs*} of value cur | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/reaches.scala:38:31 ----------------------------------------------------------- -38 | val next: () => Unit = cur.head // error +-- Error: tests/neg-custom-args/captures/reaches.scala:37:31 ----------------------------------------------------------- +37 | val next: () => Unit = cur.head // error | ^^^^^^^^ | The expression's type box () => Unit is not allowed to capture the root capability `cap`. | This usually means that a capability persists longer than its allowed lifetime. --- Error: tests/neg-custom-args/captures/reaches.scala:45:35 ----------------------------------------------------------- -45 | val next: () => Unit = cur.get.head // error +-- Error: tests/neg-custom-args/captures/reaches.scala:44:35 ----------------------------------------------------------- +44 | val next: () => Unit = cur.get.head // error | ^^^^^^^^^^^^ | The expression's type box () => Unit is not allowed to capture the root capability `cap`. | This usually means that a capability persists longer than its allowed lifetime. --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:53:2 --------------------------------------- -53 | val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:52:2 --------------------------------------- +52 | val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error | ^ | Found: box () => Unit | Required: () => Unit | | Note that box () => Unit cannot be box-converted to () => Unit | since at least one of their capture sets contains the root capability `cap` -54 | usingFile: f => -55 | id(() => f.write()) +53 | usingFile: f => +54 | id(() => f.write()) | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:62:27 -------------------------------------- -62 | val f1: File^{id*} = id(f) // error, since now id(f): File^ // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:61:27 -------------------------------------- +61 | val f1: File^{id*} = id(f) // error, since now id(f): File^ // error | ^^^^^ | Found: File^{f} | Required: File^{id*} | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/reaches.scala:61:31 ----------------------------------------------------------- -61 | val leaked = usingFile[File^{id*}]: f => // error +-- Error: tests/neg-custom-args/captures/reaches.scala:60:31 ----------------------------------------------------------- +60 | val leaked = usingFile[File^{id*}]: f => // error | ^^^ | id* cannot be tracked since its capture set is empty --- Error: tests/neg-custom-args/captures/reaches.scala:62:18 ----------------------------------------------------------- -62 | val f1: File^{id*} = id(f) // error, since now id(f): File^ // error +-- Error: tests/neg-custom-args/captures/reaches.scala:61:18 ----------------------------------------------------------- +61 | val f1: File^{id*} = id(f) // error, since now id(f): File^ // error | ^^^ | id* cannot be tracked since its capture set is empty diff --git a/tests/neg-custom-args/captures/reaches.scala b/tests/neg-custom-args/captures/reaches.scala index 50731fbd0fb2..b10ccf5eaef3 100644 --- a/tests/neg-custom-args/captures/reaches.scala +++ b/tests/neg-custom-args/captures/reaches.scala @@ -1,4 +1,3 @@ -import caps.unbox class File: def write(): Unit = ??? @@ -11,7 +10,7 @@ class Ref[T](init: T): def get: T = x def set(y: T) = { x = y } -def runAll0(@unbox xs: List[Proc]): Unit = +def runAll0(xs: List[Proc]): Unit = var cur: List[() ->{xs*} Unit] = xs while cur.nonEmpty do val next: () ->{xs*} Unit = cur.head @@ -21,7 +20,7 @@ def runAll0(@unbox xs: List[Proc]): Unit = usingFile: f => cur = (() => f.write()) :: Nil // error -def runAll1(@unbox xs: List[Proc]): Unit = +def runAll1(xs: List[Proc]): Unit = val cur = Ref[List[() ->{xs*} Unit]](xs) // OK, by revised VAR while cur.get.nonEmpty do val next: () ->{xs*} Unit = cur.get.head diff --git a/tests/neg-custom-args/captures/spread-problem.scala b/tests/neg-custom-args/captures/spread-problem.scala index 579c7817b9c1..891bbc2b2f9f 100644 --- a/tests/neg-custom-args/captures/spread-problem.scala +++ b/tests/neg-custom-args/captures/spread-problem.scala @@ -2,7 +2,7 @@ import language.experimental.captureChecking trait Source[+T] -def race[T](@caps.unbox sources: (Source[T]^)*): Source[T]^{sources*} = ??? +def race[T](sources: (Source[T]^)*): Source[T]^{sources*} = ??? def raceTwo[T](src1: Source[T]^, src2: Source[T]^): Source[T]^{} = race(Seq(src1, src2)*) // error diff --git a/tests/pos-custom-args/captures/dep-reach.scala b/tests/pos-custom-args/captures/dep-reach.scala index c81197aa738d..56343fbf8e53 100644 --- a/tests/pos-custom-args/captures/dep-reach.scala +++ b/tests/pos-custom-args/captures/dep-reach.scala @@ -1,10 +1,9 @@ -import caps.unbox object Test: class C type Proc = () => Unit def f(c: C^, d: C^): () ->{c, d} Unit = - def foo(@unbox xs: Proc*): () ->{xs*} Unit = + def foo(xs: Proc*): () ->{xs*} Unit = xs.head val a: () ->{c} Unit = () => () val b: () ->{d} Unit = () => () @@ -13,7 +12,7 @@ object Test: def g(c: C^, d: C^): () ->{c, d} Unit = - def foo(@unbox xs: Seq[() => Unit]): () ->{xs*} Unit = + def foo(xs: Seq[() => Unit]): () ->{xs*} Unit = xs.head val a: () ->{c} Unit = () => () diff --git a/tests/pos-custom-args/captures/i20503.scala b/tests/pos-custom-args/captures/i20503.scala index 5fb013391b11..c7aa2b02b81f 100644 --- a/tests/pos-custom-args/captures/i20503.scala +++ b/tests/pos-custom-args/captures/i20503.scala @@ -1,5 +1,4 @@ import language.experimental.captureChecking -import caps.unbox class List[+A]: def head: A = ??? @@ -8,7 +7,7 @@ class List[+A]: def foreach[U](f: A => U): Unit = ??? def nonEmpty: Boolean = ??? -def runOps(@unbox ops: List[() => Unit]): Unit = +def runOps(ops: List[() => Unit]): Unit = // See i20156, due to limitation in expressiveness of current system, // we could map over the list of impure elements. OK with existentials. ops.foreach(op => op()) diff --git a/tests/pos-custom-args/captures/reaches.scala b/tests/pos-custom-args/captures/reaches.scala index ab0da9b67d18..f45dc5a430ec 100644 --- a/tests/pos-custom-args/captures/reaches.scala +++ b/tests/pos-custom-args/captures/reaches.scala @@ -1,4 +1,3 @@ -import caps.unbox class C def f(xs: List[C^]) = @@ -22,7 +21,7 @@ extension [A](x: A) def :: (xs: List[A]): List[A] = ??? object Nil extends List[Nothing] -def runAll(@unbox xs: List[Proc]): Unit = +def runAll(xs: List[Proc]): Unit = var cur: List[() ->{xs*} Unit] = xs // OK, by revised VAR while cur.nonEmpty do val next: () ->{xs*} Unit = cur.head diff --git a/tests/pos/cc-poly-source-capability.scala b/tests/pos/cc-poly-source-capability.scala index 3b6c0bde1398..9a21b2d5b802 100644 --- a/tests/pos/cc-poly-source-capability.scala +++ b/tests/pos/cc-poly-source-capability.scala @@ -1,7 +1,6 @@ import language.experimental.captureChecking import annotation.experimental import caps.{CapSet, Capability} -import caps.unbox @experimental object Test: @@ -18,7 +17,7 @@ import caps.unbox def allListeners: Set[Listener^{X^}] = listeners - def test1(async1: Async, @unbox others: List[Async]) = + def test1(async1: Async, others: List[Async]) = val src = Source[CapSet^{async1, others*}] val lst1 = listener(async1) val lsts = others.map(listener) diff --git a/tests/pos/cc-poly-source.scala b/tests/pos/cc-poly-source.scala index 4cfbbaa06936..939f1f682dc8 100644 --- a/tests/pos/cc-poly-source.scala +++ b/tests/pos/cc-poly-source.scala @@ -1,7 +1,6 @@ import language.experimental.captureChecking import annotation.experimental import caps.{CapSet, Capability} -import caps.unbox @experimental object Test: @@ -25,7 +24,7 @@ import caps.unbox val ls = src.allListeners val _: Set[Listener^{lbl1, lbl2}] = ls - def test2(@unbox lbls: List[Label^]) = + def test2(lbls: List[Label^]) = def makeListener(lbl: Label^): Listener^{lbl} = ??? val listeners = lbls.map(makeListener) val src = Source[CapSet^{lbls*}] diff --git a/tests/pos/gears-probem-1.scala b/tests/pos/gears-probem-1.scala index ab71616b72fc..f5c7fdfd0a3c 100644 --- a/tests/pos/gears-probem-1.scala +++ b/tests/pos/gears-probem-1.scala @@ -1,5 +1,4 @@ import language.experimental.captureChecking -import caps.unbox trait Future[+T]: def await: T @@ -17,7 +16,7 @@ class Result[+T, +E]: case class Err[+E](e: E) extends Result[Nothing, E] case class Ok[+T](x: T) extends Result[T, Nothing] -extension [T](@unbox fs: Seq[Future[T]^]) +extension [T](fs: Seq[Future[T]^]) def awaitAll = val collector//: Collector[T]{val futures: Seq[Future[T]^{fs*}]} = Collector(fs) diff --git a/tests/pos/i13541.scala b/tests/pos/i13541.scala index 36ec09409b17..b93620badbdb 100644 --- a/tests/pos/i13541.scala +++ b/tests/pos/i13541.scala @@ -6,7 +6,7 @@ object Z: type Foo[B] = [A] =>> Bar[A, B] trait Bar[A, B] -given fooUnit[A: F]: Foo[Unit][A] = ??? +given fooUnit: [A: F] => Foo[Unit][A] = ??? //given bar[A: F]: Bar[A, Unit] = ??? def f[A: F](using Foo[Unit][A]): Nothing = ??? diff --git a/tests/pos/i18699.scala b/tests/pos/i18699.scala index 1937d7dca8c5..40c0c0ed791a 100644 --- a/tests/pos/i18699.scala +++ b/tests/pos/i18699.scala @@ -1,9 +1,8 @@ import language.experimental.captureChecking -import caps.unbox trait Cap: def use: Int = 42 -def test2(@unbox cs: List[Cap^]): Unit = +def test2(cs: List[Cap^]): Unit = val t0: Cap^{cs*} = cs.head // error var t1: Cap^{cs*} = cs.head // error diff --git a/tests/pos/i20342.scala b/tests/pos/i20342.scala index 250839680174..b6ed65428d48 100644 --- a/tests/pos/i20342.scala +++ b/tests/pos/i20342.scala @@ -1,8 +1,8 @@ class Repo[EC, E](using defaults: RepoDefaults[EC, E]) trait RepoDefaults[EC, E] object RepoDefaults: - inline given genImmutableRepo[E: DbCodec]: RepoDefaults[E, E] = ??? - inline given genRepo[EC: DbCodec, E: DbCodec]: RepoDefaults[EC, E] = ??? + inline given genImmutableRepo: [E: DbCodec] => RepoDefaults[E, E] = ??? + inline given genRepo: [EC: DbCodec, E: DbCodec] => RepoDefaults[EC, E] = ??? trait DbCodec[E] diff --git a/tests/pos/reach-capability.scala b/tests/pos/reach-capability.scala index 50ea479ec3c1..c444f14c1e22 100644 --- a/tests/pos/reach-capability.scala +++ b/tests/pos/reach-capability.scala @@ -1,7 +1,6 @@ import language.experimental.captureChecking import annotation.experimental import caps.Capability -import caps.unbox @experimental object Test2: @@ -12,7 +11,7 @@ import caps.unbox class Listener - def test2(@unbox lbls: List[Label]) = + def test2(lbls: List[Label]) = def makeListener(lbl: Label): Listener^{lbl} = ??? val listeners = lbls.map(makeListener) // should work diff --git a/tests/pos/reach-problem.scala b/tests/pos/reach-problem.scala index d6b7b79011a6..19f7a50e5387 100644 --- a/tests/pos/reach-problem.scala +++ b/tests/pos/reach-problem.scala @@ -1,11 +1,10 @@ import language.experimental.captureChecking -import caps.unbox class Box[T](items: Seq[T^]): def getOne: T^{items*} = ??? object Box: - def getOne[T](@unbox items: Seq[T^]): T^{items*} = + def getOne[T](items: Seq[T^]): T^{items*} = val bx = Box(items) bx.getOne /* From e0075399089c6526404d85b4faafd2c608902c09 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 14 Oct 2024 17:19:54 +0200 Subject: [PATCH 14/17] Fix computation of deep capture set. A deep capture set should not be shortened to a reach capability `x*` if there are elements in the underlying set that live longer than `x`. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 34 ++++++++++++++----- .../dotty/tools/dotc/cc/CheckCaptures.scala | 9 ++--- .../captures/delayedRunops.check | 5 +++ .../captures/delayedRunops.scala | 15 ++++++++ .../captures/delayedRunops2.scala | 10 ++++++ 5 files changed, 58 insertions(+), 15 deletions(-) create mode 100644 tests/neg-custom-args/captures/delayedRunops.check create mode 100644 tests/neg-custom-args/captures/delayedRunops.scala create mode 100644 tests/neg-custom-args/captures/delayedRunops2.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index d31afe200263..37be4c76c8d1 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -222,19 +222,27 @@ extension (tp: Type) case tp: SingletonCaptureRef => tp.captureSetOfInfo case _ => CaptureSet.ofType(tp, followResult = false) - /** The deep capture set of a type. - * For singleton capabilities `x` and reach capabilities `x*`, this is `{x*}`, provided - * the underlying capture set resulting from traversing the type is non-empty. - * For other types this is the union of all covariant capture sets embedded - * in the type, as computed by `CaptureSet.ofTypeDeeply`. + /** The deep capture set of a type. This is by default the union of all + * covariant capture sets embedded in the widened type, as computed by + * `CaptureSet.ofTypeDeeply`. If that set is nonempty, and the type is + * a singleton capability `x` or a reach capability `x*`, the deep capture + * set can be narrowed to`{x*}`. However, A deep capture set should not be + * narrowed to a reach capability `x*` if there are elements in the underlying + * set that live longer than `x`. See `delayedRunops.scala` for a test case. */ def deepCaptureSet(using Context): CaptureSet = val dcs = CaptureSet.ofTypeDeeply(tp.widen.stripCapturing) + def reachCanSubsumDcs = + dcs.isUniversal + || dcs.elems.forall(c => c.pathOwner.isContainedIn(tp.pathOwner)) if dcs.isAlwaysEmpty then tp.captureSet else tp match - case tp @ ReachCapability(_) => tp.singletonCaptureSet - case tp: SingletonCaptureRef if tp.isTrackableRef => tp.reach.singletonCaptureSet - case _ => tp.captureSet ++ dcs + case tp @ ReachCapability(_) if reachCanSubsumDcs => + tp.singletonCaptureSet + case tp: SingletonCaptureRef if tp.isTrackableRef && reachCanSubsumDcs => + tp.reach.singletonCaptureSet + case _ => + tp.captureSet ++ dcs /** A type capturing `ref` */ def capturing(ref: CaptureRef)(using Context): Type = @@ -277,8 +285,18 @@ extension (tp: Type) /** The first element of this path type */ final def pathRoot(using Context): Type = tp.dealias match case tp1: NamedType if tp1.symbol.owner.isClass => tp1.prefix.pathRoot + case tp1 @ ReachCapability(tp2) => tp2.pathRoot case _ => tp + /** If this part starts with `C.this`, the class `C`. + * Otherwise, if it starts with a reference `r`, `r`'s owner. + * Otherwise NoSymbol. + */ + final def pathOwner(using Context): Symbol = pathRoot match + case tp1: NamedType => tp1.symbol.owner + case tp1: ThisType => tp1.cls + case _ => NoSymbol + /** If this is a unboxed capturing type with nonempty capture set, its boxed version. * Or, if type is a TypeBounds of capturing types, the version where the bounds are boxed. * The identity for all other types. diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 66add1e7d9e3..af1e3c9cd6e8 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1090,13 +1090,8 @@ class CheckCaptures extends Recheck, SymTransformer: (erefs /: erefs.elems): (erefs, eref) => eref match case eref: ThisType if isPureContext(ctx.owner, eref.cls) => - def isOuterRef(aref: Type): Boolean = aref.pathRoot match - case aref: NamedType => eref.cls.isProperlyContainedIn(aref.symbol.owner) - case aref: ThisType => eref.cls.isProperlyContainedIn(aref.cls) - case _ => false - - val outerRefs = arefs.filter(isOuterRef) - + val outerRefs = arefs.filter: aref => + eref.cls.isProperlyContainedIn(aref.pathOwner) // Include implicitly added outer references in the capture set of the class of `eref`. for outerRef <- outerRefs.elems do if !erefs.elems.contains(outerRef) diff --git a/tests/neg-custom-args/captures/delayedRunops.check b/tests/neg-custom-args/captures/delayedRunops.check new file mode 100644 index 000000000000..625f17663a4a --- /dev/null +++ b/tests/neg-custom-args/captures/delayedRunops.check @@ -0,0 +1,5 @@ +-- Error: tests/neg-custom-args/captures/delayedRunops.scala:15:13 ----------------------------------------------------- +15 | runOps(ops1) // error + | ^^^^ + | reference ops* is not included in the allowed capture set {} + | of an enclosing function literal with expected type () -> Unit diff --git a/tests/neg-custom-args/captures/delayedRunops.scala b/tests/neg-custom-args/captures/delayedRunops.scala new file mode 100644 index 000000000000..1d1475806c52 --- /dev/null +++ b/tests/neg-custom-args/captures/delayedRunops.scala @@ -0,0 +1,15 @@ +import language.experimental.captureChecking + + // ok + def runOps(ops: List[() => Unit]): Unit = + ops.foreach(op => op()) + + // ok + def delayedRunOps(ops: List[() => Unit]): () ->{ops*} Unit = + () => runOps(ops) + + // unsound: impure operation pretended pure + def delayedRunOps1(ops: List[() => Unit]): () ->{} Unit = + () => + val ops1 = ops + runOps(ops1) // error diff --git a/tests/neg-custom-args/captures/delayedRunops2.scala b/tests/neg-custom-args/captures/delayedRunops2.scala new file mode 100644 index 000000000000..0c9aec20aaa3 --- /dev/null +++ b/tests/neg-custom-args/captures/delayedRunops2.scala @@ -0,0 +1,10 @@ +import language.experimental.captureChecking + +def runOps(ops: List[() => Unit]): Unit = + ops.foreach(op => op()) + +def app[T, U](x: T, op: T => U): () ->{op} U = + () => op(x) + +def unsafeRunOps(ops: List[() => Unit]): () ->{} Unit = + app[List[() ->{ops*} Unit], Unit](ops, runOps) // error From c6e3910e106107ed3a43e3582135c5c544709d59 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 15 Oct 2024 18:24:44 +0200 Subject: [PATCH 15/17] Handle reach capabilities correctly in markFree The correct point to address charging reach capabilities is in markFree itself: When a reach capability goes out of scope, and that capability is not a parameter, we need to continue with the underlying capture set. With this fix, we don't need any special provisions to compute deep capture sets anymore. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 18 +++-- .../dotty/tools/dotc/cc/CheckCaptures.scala | 76 +++++++++++-------- compiler/src/dotty/tools/dotc/cc/Setup.scala | 3 +- .../captures/delayedRunops.check | 10 +++ .../captures/delayedRunops.scala | 12 +++ .../captures/delayedRunops2.scala | 10 --- tests/neg-custom-args/captures/reaches.check | 4 +- .../neg-custom-args/captures/wf-reach-1.check | 4 + .../neg-custom-args/captures/wf-reach-1.scala | 2 + tests/neg/leak-problem.scala | 4 +- .../captures/gears-problem.scala} | 0 11 files changed, 89 insertions(+), 54 deletions(-) delete mode 100644 tests/neg-custom-args/captures/delayedRunops2.scala create mode 100644 tests/neg-custom-args/captures/wf-reach-1.check create mode 100644 tests/neg-custom-args/captures/wf-reach-1.scala rename tests/{pos/gears-probem.scala => pos-custom-args/captures/gears-problem.scala} (100%) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 37be4c76c8d1..093db1978292 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -226,20 +226,15 @@ extension (tp: Type) * covariant capture sets embedded in the widened type, as computed by * `CaptureSet.ofTypeDeeply`. If that set is nonempty, and the type is * a singleton capability `x` or a reach capability `x*`, the deep capture - * set can be narrowed to`{x*}`. However, A deep capture set should not be - * narrowed to a reach capability `x*` if there are elements in the underlying - * set that live longer than `x`. See `delayedRunops.scala` for a test case. + * set can be narrowed to`{x*}`. */ def deepCaptureSet(using Context): CaptureSet = val dcs = CaptureSet.ofTypeDeeply(tp.widen.stripCapturing) - def reachCanSubsumDcs = - dcs.isUniversal - || dcs.elems.forall(c => c.pathOwner.isContainedIn(tp.pathOwner)) if dcs.isAlwaysEmpty then tp.captureSet else tp match - case tp @ ReachCapability(_) if reachCanSubsumDcs => + case tp @ ReachCapability(_) => tp.singletonCaptureSet - case tp: SingletonCaptureRef if tp.isTrackableRef && reachCanSubsumDcs => + case tp: SingletonCaptureRef if tp.isTrackableRef => tp.reach.singletonCaptureSet case _ => tp.captureSet ++ dcs @@ -297,6 +292,13 @@ extension (tp: Type) case tp1: ThisType => tp1.cls case _ => NoSymbol + final def isParamPath(using Context): Boolean = tp.dealias match + case tp1: NamedType => + tp1.prefix match + case _: ThisType | NoPrefix => tp1.symbol.isOneOf(Param | ParamAccessor) + case prefix => prefix.isParamPath + case _ => false + /** If this is a unboxed capturing type with nonempty capture set, its boxed version. * Or, if type is a TypeBounds of capturing types, the version where the bounds are boxed. * The identity for all other types. diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index af1e3c9cd6e8..713dfd531193 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -328,20 +328,22 @@ class CheckCaptures extends Recheck, SymTransformer: then CaptureSet.Var(sym.owner, level = sym.ccLevel) else CaptureSet.empty) - /** For all nested environments up to `limit` or a closed environment perform `op`, - * but skip environmenrts directly enclosing environments of kind ClosureResult. + /** The next environment enclosing `env` that needs to be charged + * with free references. + * Skips environments directly enclosing environments of kind ClosureResult. + * @param included Whether an environment is included in the range of + * environments to charge. Once `included` is false, no + * more environments need to be charged. */ - def forallOuterEnvsUpTo(limit: Symbol)(op: Env => Unit)(using Context): Unit = - def recur(env: Env, skip: Boolean): Unit = - if env.isOpen && env.owner != limit then - if !skip then op(env) - if !env.isOutermost then - var nextEnv = env.outer - if env.owner.isConstructor then - if nextEnv.owner != limit && !nextEnv.isOutermost then - nextEnv = nextEnv.outer - recur(nextEnv, skip = env.kind == EnvKind.ClosureResult) - recur(curEnv, skip = false) + def nextEnvToCharge(env: Env, included: Env => Boolean)(using Context): Env = + var nextEnv = env.outer + if env.owner.isConstructor then + if included(nextEnv) then nextEnv = nextEnv.outer + if env.kind == EnvKind.ClosureResult then + // skip this one + nextEnvToCharge(nextEnv, included) + else + nextEnv /** A description where this environment comes from */ private def provenance(env: Env)(using Context): String = @@ -355,7 +357,6 @@ class CheckCaptures extends Recheck, SymTransformer: else i"\nof the enclosing ${owner.showLocated}" - /** Include `sym` in the capture sets of all enclosing environments nested in the * the environment in which `sym` is defined. */ @@ -364,9 +365,12 @@ class CheckCaptures extends Recheck, SymTransformer: def markFree(sym: Symbol, ref: TermRef, pos: SrcPos)(using Context): Unit = if sym.exists && ref.isTracked then - forallOuterEnvsUpTo(sym.enclosure): env => - capt.println(i"Mark $sym with cs ${ref.captureSet} free in ${env.owner}") - checkElem(ref, env.captured, pos, provenance(env)) + def recur(env: Env): Unit = + if env.isOpen && env.owner != sym.enclosure then + capt.println(i"Mark $sym with cs ${ref.captureSet} free in ${env.owner}") + checkElem(ref, env.captured, pos, provenance(env)) + recur(nextEnvToCharge(env, _.owner != sym.enclosure)) + recur(curEnv) /** Make sure (projected) `cs` is a subset of the capture sets of all enclosing * environments. At each stage, only include references from `cs` that are outside @@ -381,20 +385,30 @@ class CheckCaptures extends Recheck, SymTransformer: else !sym.isContainedIn(env.owner) - def checkSubsetEnv(cs: CaptureSet, env: Env)(using Context): Unit = - // Only captured references that are visible from the environment - // should be included. - val included = cs.filter: c => - c.stripReach.pathRoot match - case ref: NamedType => isVisibleFromEnv(ref.symbol.owner, env) - case ref: ThisType => isVisibleFromEnv(ref.cls, env) - case _ => false - checkSubset(included, env.captured, pos, provenance(env)) - capt.println(i"Include call or box capture $included from $cs in ${env.owner} --> ${env.captured}") - - if !cs.isAlwaysEmpty then - forallOuterEnvsUpTo(ctx.owner.topLevelClass): env => - checkSubsetEnv(cs, env) + def recur(cs: CaptureSet, env: Env)(using Context): Unit = + if env.isOpen && !env.owner.isStaticOwner && !cs.isAlwaysEmpty then + // Only captured references that are visible from the environment + // should be included. + val included = cs.filter: c => + val isVisible = c.stripReach.pathRoot match + case ref: NamedType => isVisibleFromEnv(ref.symbol.owner, env) + case ref: ThisType => isVisibleFromEnv(ref.cls, env) + case _ => false + c match + case ReachCapability(c1) if !isVisible && !c1.isParamPath => + // When a reach capabilty x* where `x` is not a parameter goes out + // of scope, we need to continue with `x`'s underlying deep capture set. + // The same is not an issue for normal capabilities since in a local + // definition `val x = e`, the capabilities of `e` have already been charged. + val underlying = CaptureSet.ofTypeDeeply(c1.widen) + capt.println(i"Widen reach $c to $underlying in ${env.owner}") + recur(underlying, env) + case _ => + isVisible + checkSubset(included, env.captured, pos, provenance(env)) + capt.println(i"Include call or box capture $included from $cs in ${env.owner} --> ${env.captured}") + recur(included, nextEnvToCharge(env, !_.owner.isStaticOwner)) + recur(cs, curEnv) end markFree /** Include references captured by the called method in the current environment stack */ diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 76ae41649517..ddd9e240be5d 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -751,7 +751,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: report.warning(em"redundant capture: $dom already accounts for $ref", pos) if ref.captureSetOfInfo.elems.isEmpty && !ref.derivesFrom(defn.Caps_Capability) then - report.error(em"$ref cannot be tracked since its capture set is empty", pos) + val deepStr = if ref.isReach then " deep" else "" + report.error(em"$ref cannot be tracked since its$deepStr capture set is empty", pos) check(parent.captureSet, parent) val others = diff --git a/tests/neg-custom-args/captures/delayedRunops.check b/tests/neg-custom-args/captures/delayedRunops.check index 625f17663a4a..179d4e62a0e4 100644 --- a/tests/neg-custom-args/captures/delayedRunops.check +++ b/tests/neg-custom-args/captures/delayedRunops.check @@ -3,3 +3,13 @@ | ^^^^ | reference ops* is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> Unit +-- Error: tests/neg-custom-args/captures/delayedRunops.scala:21:13 ----------------------------------------------------- +21 | runOps(ops1) // error + | ^^^^ + | reference (caps.cap : caps.Capability) is not included in the allowed capture set {} + | of an enclosing function literal with expected type () -> Unit +-- Error: tests/neg-custom-args/captures/delayedRunops.scala:27:13 ----------------------------------------------------- +27 | runOps(ops1) // error + | ^^^^ + | reference ops* is not included in the allowed capture set {} + | of an enclosing function literal with expected type () -> Unit diff --git a/tests/neg-custom-args/captures/delayedRunops.scala b/tests/neg-custom-args/captures/delayedRunops.scala index 1d1475806c52..8fcaf87b3b53 100644 --- a/tests/neg-custom-args/captures/delayedRunops.scala +++ b/tests/neg-custom-args/captures/delayedRunops.scala @@ -13,3 +13,15 @@ import language.experimental.captureChecking () => val ops1 = ops runOps(ops1) // error + + // unsound: impure operation pretended pure + def delayedRunOps2(ops: List[() => Unit]): () ->{} Unit = + () => + val ops1: List[() => Unit] = ops + runOps(ops1) // error + + // unsound: impure operation pretended pure + def delayedRunOps3(ops: List[() => Unit]): () ->{} Unit = + () => + val ops1: List[() ->{ops*} Unit] = ops + runOps(ops1) // error diff --git a/tests/neg-custom-args/captures/delayedRunops2.scala b/tests/neg-custom-args/captures/delayedRunops2.scala deleted file mode 100644 index 0c9aec20aaa3..000000000000 --- a/tests/neg-custom-args/captures/delayedRunops2.scala +++ /dev/null @@ -1,10 +0,0 @@ -import language.experimental.captureChecking - -def runOps(ops: List[() => Unit]): Unit = - ops.foreach(op => op()) - -def app[T, U](x: T, op: T => U): () ->{op} U = - () => op(x) - -def unsafeRunOps(ops: List[() => Unit]): () ->{} Unit = - app[List[() ->{ops*} Unit], Unit](ops, runOps) // error diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index d5facb0f8529..d9a6e58b9938 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -47,8 +47,8 @@ -- Error: tests/neg-custom-args/captures/reaches.scala:60:31 ----------------------------------------------------------- 60 | val leaked = usingFile[File^{id*}]: f => // error | ^^^ - | id* cannot be tracked since its capture set is empty + | id* cannot be tracked since its deep capture set is empty -- Error: tests/neg-custom-args/captures/reaches.scala:61:18 ----------------------------------------------------------- 61 | val f1: File^{id*} = id(f) // error, since now id(f): File^ // error | ^^^ - | id* cannot be tracked since its capture set is empty + | id* cannot be tracked since its deep capture set is empty diff --git a/tests/neg-custom-args/captures/wf-reach-1.check b/tests/neg-custom-args/captures/wf-reach-1.check new file mode 100644 index 000000000000..6a3ac9771a11 --- /dev/null +++ b/tests/neg-custom-args/captures/wf-reach-1.check @@ -0,0 +1,4 @@ +-- Error: tests/neg-custom-args/captures/wf-reach-1.scala:2:17 --------------------------------------------------------- +2 | val y: Object^{x*} = ??? // error + | ^^ + | x* cannot be tracked since its deep capture set is empty diff --git a/tests/neg-custom-args/captures/wf-reach-1.scala b/tests/neg-custom-args/captures/wf-reach-1.scala new file mode 100644 index 000000000000..c8901c7ae4a8 --- /dev/null +++ b/tests/neg-custom-args/captures/wf-reach-1.scala @@ -0,0 +1,2 @@ +def test(x: List[() -> Unit]) = + val y: Object^{x*} = ??? // error diff --git a/tests/neg/leak-problem.scala b/tests/neg/leak-problem.scala index c271e5d02560..5af66491e80b 100644 --- a/tests/neg/leak-problem.scala +++ b/tests/neg/leak-problem.scala @@ -27,8 +27,8 @@ def test(): Unit = t1.read() val xsLambda2 = () => useBoxedAsync2(xs) - val _: () ->{xs*} Unit = xsLambda - val _: () -> Unit = xsLambda // error + val _: () ->{xs*} Unit = xsLambda2 + val _: () -> Unit = xsLambda2 // error val f: Box[Async^] => Unit = (x: Box[Async^]) => useBoxedAsync(x) diff --git a/tests/pos/gears-probem.scala b/tests/pos-custom-args/captures/gears-problem.scala similarity index 100% rename from tests/pos/gears-probem.scala rename to tests/pos-custom-args/captures/gears-problem.scala From f6f996cc99bb44e47ea6a611ef4659bf56b086e5 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 21 Oct 2024 19:19:53 +0200 Subject: [PATCH 16/17] Add implied captures in function types This is an attempt to fix the problem explified in the `delayedRunops*.scala` tests. We can treat it as a baseline that fixes the immediate problem of the interaction of reach capabilities and type variables. There might be better ways to do this by using a proper adapation rule for function types instead of adding implied captures post-hoc. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 8 +++++ .../src/dotty/tools/dotc/cc/CaptureSet.scala | 33 +++++++++++++++++++ .../dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- .../src/scala/collection/SeqView.scala | 2 +- .../captures/delayedRunops2.check | 14 ++++++++ .../captures/delayedRunops2.scala | 22 +++++++++++++ .../captures/delayedRunops3.check | 10 ++++++ .../captures/delayedRunops3.scala | 14 ++++++++ .../captures/delayedRunops4.check | 21 ++++++++++++ .../captures/delayedRunops4.scala | 24 ++++++++++++++ tests/neg-custom-args/captures/i21401.check | 7 ++++ tests/neg-custom-args/captures/i21401.scala | 2 +- .../neg-custom-args/captures/outer-var.check | 10 ++++++ .../neg-custom-args/captures/outer-var.scala | 2 +- tests/neg-custom-args/captures/reaches.check | 13 +++----- tests/neg-custom-args/captures/reaches2.check | 18 +++++----- tests/neg-custom-args/captures/reaches2.scala | 2 +- tests/neg/leak-problem.scala | 2 +- tests/pos-custom-args/captures/i20503.scala | 6 ++-- .../captures/unsafe-unbox.scala | 3 +- tests/pos/cc-poly-source-capability.scala | 9 +++++ tests/pos/cc-poly-source.scala | 6 ++++ 22 files changed, 200 insertions(+), 30 deletions(-) create mode 100644 tests/neg-custom-args/captures/delayedRunops2.check create mode 100644 tests/neg-custom-args/captures/delayedRunops2.scala create mode 100644 tests/neg-custom-args/captures/delayedRunops3.check create mode 100644 tests/neg-custom-args/captures/delayedRunops3.scala create mode 100644 tests/neg-custom-args/captures/delayedRunops4.check create mode 100644 tests/neg-custom-args/captures/delayedRunops4.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 093db1978292..80d853efa56f 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -527,6 +527,14 @@ extension (tp: Type) case _ => tp + /** Add implied captures as defined by `CaptureSet.addImplied`. */ + def withImpliedCaptures(using Context): Type = + if tp.isValueType && !tp.isAlwaysPure then + val implied = CaptureSet.addImplied()(CaptureSet.empty, tp) + if !implied.isAlwaysEmpty then capt.println(i"Add implied $implied to $tp") + tp.capturing(implied) + else tp + def level(using Context): Level = tp match case tp: TermRef => tp.symbol.ccLevel diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 835e413463bd..51d1328d2ad6 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -1068,6 +1068,39 @@ object CaptureSet: .showing(i"Deep capture set of $ref: ${ref1.widen} = $result", capt) case _ => ofType(ref.underlying, followResult = true) + /** Add captures implied by a type. This means: if we have a contravarint, boxed + * capability in a function parameter and the capability is either `cap`, or a + * reach capability, or a capture set variable, add the same capability to the enclosing + * function arrow. For instance `List[() ->{ops*} Unit] -> Unit` would become + * `List[() ->{ops*} Unit] ->{ops*} Unit`. This is needed to make + * the `delayedRunops*.scala` tests produce errors. + * TODO: Investigate whether we can roll this into a widening rule like + * + * List[() ->{cap} Unit] -> Unit <: List[() ->{ops*} Unit] ->{ops*} Unit + * + * but not + * + * List[() ->{cap} Unit] -> Unit <: List[() ->{ops*} Unit] -> Unit + * + * It would mean that a reach capability can no longer be a subtype of `cap`. + */ + class addImplied(using Context) extends TypeAccumulator[CaptureSet]: + var boundVars: Set[CaptureRef] = Set.empty + def isImplied(tp: CaptureRef) = + (tp.isRootCapability || tp.isReach || tp.derivesFrom(defn.Caps_CapSet)) + && !boundVars.contains(tp.stripReach) + def apply(cs: CaptureSet, t: Type) = t match + case t @ CapturingType(parent, cs1) => + val cs2 = this(cs, parent) + if variance <= 0 && t.isBoxed then cs2 ++ cs1.filter(isImplied) + else cs2 + case t: MethodOrPoly => + val saved = boundVars + boundVars ++= t.paramRefs.asInstanceOf[List[CaptureRef]] + try foldOver(cs, t) finally boundVars = saved + case _ => + foldOver(cs, t) + /** Capture set of a type */ def ofType(tp: Type, followResult: Boolean)(using Context): CaptureSet = def recur(tp: Type): CaptureSet = trace(i"ofType $tp, ${tp.getClass} $followResult", show = true): diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 713dfd531193..ecd016aadb9a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1292,7 +1292,7 @@ class CheckCaptures extends Recheck, SymTransformer: else val widened = improveCaptures(actual.widen.dealiasKeepAnnots, actual) val adapted = adaptBoxed( - widened.withReachCaptures(actual), expected, pos, + widened.withReachCaptures(actual).withImpliedCaptures, expected, pos, covariant = true, alwaysConst = false, boxErrors) if adapted eq widened then actual else adapted.showing(i"adapt boxed $actual vs $expected = $adapted", capt) diff --git a/scala2-library-cc/src/scala/collection/SeqView.scala b/scala2-library-cc/src/scala/collection/SeqView.scala index 292dc61ddaa8..d1befe3f0988 100644 --- a/scala2-library-cc/src/scala/collection/SeqView.scala +++ b/scala2-library-cc/src/scala/collection/SeqView.scala @@ -212,7 +212,7 @@ object SeqView { override def sorted[B1 >: A](implicit ord1: Ordering[B1]): SeqView[A]^{this} = if (ord1 == Sorted.this.ord) outer.unsafeAssumePure else if (ord1.isReverseOf(Sorted.this.ord)) this - else new Sorted(elems, len, ord1) + else new Sorted(elems, len, ord1).asInstanceOf // !!! asInstanceOf needed after adding addImplied widening } @volatile private[this] var evaluated = false diff --git a/tests/neg-custom-args/captures/delayedRunops2.check b/tests/neg-custom-args/captures/delayedRunops2.check new file mode 100644 index 000000000000..a1f6be6f6e3b --- /dev/null +++ b/tests/neg-custom-args/captures/delayedRunops2.check @@ -0,0 +1,14 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/delayedRunops2.scala:10:35 ------------------------------- +10 | app[List[() ->{ops*} Unit], Unit](ops, runOps) // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Found: () ->{ops*} Unit + | Required: () -> Unit + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/delayedRunops2.scala:18:36 ------------------------------- +18 | app2[List[() => Unit], Unit](ops, runOps: List[() => Unit] -> Unit) // error + | ^^^^^^ + | Found: (ops: List[box () ->? Unit]^?) ->? Unit + | Required: (ops: List[box () => Unit]) -> Unit + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/delayedRunops2.scala b/tests/neg-custom-args/captures/delayedRunops2.scala new file mode 100644 index 000000000000..ada95065764f --- /dev/null +++ b/tests/neg-custom-args/captures/delayedRunops2.scala @@ -0,0 +1,22 @@ +import language.experimental.captureChecking + +def runOps(ops: List[() => Unit]): Unit = + ops.foreach(op => op()) + +def app[T, U](x: T, op: T => U): () ->{op} U = + () => op(x) + +def unsafeRunOps(ops: List[() => Unit]): () ->{} Unit = + app[List[() ->{ops*} Unit], Unit](ops, runOps) // error + +def app2[T, U](x: T, op: T => U): () ->{op} U = + () => + def y: T = x + op(y) + +def unsafeRunOps2(ops: List[() => Unit]): () -> Unit = + app2[List[() => Unit], Unit](ops, runOps: List[() => Unit] -> Unit) // error + + + + diff --git a/tests/neg-custom-args/captures/delayedRunops3.check b/tests/neg-custom-args/captures/delayedRunops3.check new file mode 100644 index 000000000000..f2d08f4ea802 --- /dev/null +++ b/tests/neg-custom-args/captures/delayedRunops3.check @@ -0,0 +1,10 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/delayedRunops3.scala:10:41 ------------------------------- +10 | app[List[() ->{ops*} Unit], Unit](ops, runOps) // error + | ^^^^^^ + | Found: (ops: List[box () ->? Unit]^?) ->? Unit + | Required: (ops: List[box () ->{ops²*} Unit]) -> Unit + | + | where: ops is a reference to a value parameter + | ops² is a parameter in method unsafeRunOps + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/delayedRunops3.scala b/tests/neg-custom-args/captures/delayedRunops3.scala new file mode 100644 index 000000000000..fb727fa94487 --- /dev/null +++ b/tests/neg-custom-args/captures/delayedRunops3.scala @@ -0,0 +1,14 @@ +import language.experimental.captureChecking + +def runOps(ops: List[() => Unit]): Unit = + ops.foreach(op => op()) + +def app[T, U](x: T, op: T -> U): () ->{} U = + () => op(x) + +def unsafeRunOps(ops: List[() => Unit]): () ->{} Unit = + app[List[() ->{ops*} Unit], Unit](ops, runOps) // error + + + + diff --git a/tests/neg-custom-args/captures/delayedRunops4.check b/tests/neg-custom-args/captures/delayedRunops4.check new file mode 100644 index 000000000000..1cc7a3fc293a --- /dev/null +++ b/tests/neg-custom-args/captures/delayedRunops4.check @@ -0,0 +1,21 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/delayedRunops4.scala:11:4 -------------------------------- +11 | runOps[C]: List[() ->{C^} Unit] ->{C^} Unit) // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Found: List[box () ->{C^} Unit] ->{C^} Unit + | Required: List[box () ->{C^} Unit] -> Unit + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/delayedRunops4.scala:15:43 ------------------------------- +15 | app[List[() ->{C^} Unit], Unit](ops, rops[C]) // error + | ^^^^^^^ + | Found: (ops: List[box () ->{C^} Unit]) ->{C^} Unit + | Required: (ops: List[box () ->{C^} Unit]) -> Unit + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/delayedRunops4.scala:18:39 ------------------------------- +18 | app[List[() ->{C^} Unit], Unit](ops, runOps) // error + | ^^^^^^ + | Found: (ops: List[box () ->? Unit]^?) ->? Unit + | Required: (ops: List[box () ->{C^} Unit]) -> Unit + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/delayedRunops4.scala b/tests/neg-custom-args/captures/delayedRunops4.scala new file mode 100644 index 000000000000..e2b7a1bd5bb3 --- /dev/null +++ b/tests/neg-custom-args/captures/delayedRunops4.scala @@ -0,0 +1,24 @@ +import language.experimental.captureChecking + +def runOps[C^](ops: List[() ->{C^} Unit]): Unit = + ops.foreach(op => op()) + +def app[T, U](x: T, op: T -> U): () ->{} U = + () => op(x) + +def unsafeRunOps[C^](ops: List[() ->{C^} Unit]): () ->{} Unit = + app[List[() ->{C^} Unit], Unit](ops, + runOps[C]: List[() ->{C^} Unit] ->{C^} Unit) // error + +def unsafeRunOps2[C^](ops: List[() ->{C^} Unit]): () ->{} Unit = + def rops[D^]: (ops: List[() ->{D^} Unit]) -> Unit = ??? + app[List[() ->{C^} Unit], Unit](ops, rops[C]) // error + +def unsafeRunOps3[C^](ops: List[() ->{C^} Unit]): () ->{} Unit = + app[List[() ->{C^} Unit], Unit](ops, runOps) // error + + + + + + diff --git a/tests/neg-custom-args/captures/i21401.check b/tests/neg-custom-args/captures/i21401.check index a957c1ea11c5..601e426ec0bd 100644 --- a/tests/neg-custom-args/captures/i21401.check +++ b/tests/neg-custom-args/captures/i21401.check @@ -13,3 +13,10 @@ | ^^^^^^^^^^^^^^^^^^^ | The expression's type Res is not allowed to capture the root capability `cap` in its part box IO^. | This usually means that a capability persists longer than its allowed lifetime. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21401.scala:17:67 --------------------------------------- +17 | val x: Boxed[IO^] = leaked[Boxed[IO^], Boxed[IO^] -> Boxed[IO^]](x => x) // error after adding addImplied widening + | ^^^^^^ + | Found: (x: Boxed[box IO^?]^?) ->? Boxed[box IO^?]^? + | Required: (x: Boxed[box IO^]) -> Boxed[box IO^] + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i21401.scala b/tests/neg-custom-args/captures/i21401.scala index 05fbd0757c3c..48427426e5a8 100644 --- a/tests/neg-custom-args/captures/i21401.scala +++ b/tests/neg-custom-args/captures/i21401.scala @@ -14,6 +14,6 @@ def mkRes(x: IO^): Res = def test2() = val a = usingIO[IO^](x => x) // error: The expression's type IO^ is not allowed to capture the root capability `cap` val leaked: [R, X <: Boxed[IO^] -> R] -> (op: X) -> R = usingIO[Res](mkRes) // error: The expression's type Res is not allowed to capture the root capability `cap` in its part box IO^ - val x: Boxed[IO^] = leaked[Boxed[IO^], Boxed[IO^] -> Boxed[IO^]](x => x) + val x: Boxed[IO^] = leaked[Boxed[IO^], Boxed[IO^] -> Boxed[IO^]](x => x) // error after adding addImplied widening val y: IO^{x*} = x.unbox y.println("boom") diff --git a/tests/neg-custom-args/captures/outer-var.check b/tests/neg-custom-args/captures/outer-var.check index 32351a179eab..2aa3ce936ddc 100644 --- a/tests/neg-custom-args/captures/outer-var.check +++ b/tests/neg-custom-args/captures/outer-var.check @@ -28,3 +28,13 @@ | since at least one of their capture sets contains the root capability `cap` | | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:16:65 ------------------------------------ +16 | var finalizeActions = collection.mutable.ListBuffer[() => Unit]() // error under addImplied widening + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + |Found: scala.collection.mutable.ListBuffer[box () => Unit]^ + |Required: box scala.collection.mutable.ListBuffer[box () ->? Unit]^ + | + |Note that scala.collection.mutable.ListBuffer[box () => Unit]^ cannot be box-converted to box scala.collection.mutable.ListBuffer[box () ->? Unit]^ + |since at least one of their capture sets contains the root capability `cap` + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/outer-var.scala b/tests/neg-custom-args/captures/outer-var.scala index e26cd631602a..861202b5626d 100644 --- a/tests/neg-custom-args/captures/outer-var.scala +++ b/tests/neg-custom-args/captures/outer-var.scala @@ -13,6 +13,6 @@ def test(p: Proc, q: () => Unit) = y = (q: Proc) // error y = q // OK, was error under sealed - var finalizeActions = collection.mutable.ListBuffer[() => Unit]() // OK, was error under sealed + var finalizeActions = collection.mutable.ListBuffer[() => Unit]() // error under addImplied widening diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index d9a6e58b9938..8ef0241ee025 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -25,16 +25,11 @@ | ^^^^^^^^^^^^ | The expression's type box () => Unit is not allowed to capture the root capability `cap`. | This usually means that a capability persists longer than its allowed lifetime. --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:52:2 --------------------------------------- +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:52:27 -------------------------------------- 52 | val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error - | ^ - | Found: box () => Unit - | Required: () => Unit - | - | Note that box () => Unit cannot be box-converted to () => Unit - | since at least one of their capture sets contains the root capability `cap` -53 | usingFile: f => -54 | id(() => f.write()) + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | Found: Id[box () => Unit, () -> Unit]^ + | Required: Id[box () => Unit, box () => Unit] | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:61:27 -------------------------------------- diff --git a/tests/neg-custom-args/captures/reaches2.check b/tests/neg-custom-args/captures/reaches2.check index 03860ee4a01b..0853cd227feb 100644 --- a/tests/neg-custom-args/captures/reaches2.check +++ b/tests/neg-custom-args/captures/reaches2.check @@ -1,10 +1,8 @@ --- Error: tests/neg-custom-args/captures/reaches2.scala:8:10 ----------------------------------------------------------- -8 | ps.map((x, y) => compose1(x, y)) // error // error - | ^ - |reference ps* is not included in the allowed capture set {} - |of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box (x$0: A^?) ->? (ex$15: caps.Exists) -> A^? --- Error: tests/neg-custom-args/captures/reaches2.scala:8:13 ----------------------------------------------------------- -8 | ps.map((x, y) => compose1(x, y)) // error // error - | ^ - |reference ps* is not included in the allowed capture set {} - |of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box (x$0: A^?) ->? (ex$15: caps.Exists) -> A^? +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches2.scala:8:10 -------------------------------------- +8 | ps.map((x, y) => compose1(x, y)) // error + | ^^^^^^^^^^^^^^^^^^^^^^^ + |Found: (x$1: (box (x$0: A^?) ->? (ex$18: caps.Exists) -> A^?, box (x$0: A^?) ->? (ex$19: caps.Exists) -> A^?)^?) ->? + | box (x$0: A^?) ->? A^? + |Required: (x$1: (box A ->{ps*} A, box A ->{ps*} A)) -> box (x$0: A^?) ->? A^? + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/reaches2.scala b/tests/neg-custom-args/captures/reaches2.scala index f2447b8c8795..2fbc6dbf7c32 100644 --- a/tests/neg-custom-args/captures/reaches2.scala +++ b/tests/neg-custom-args/captures/reaches2.scala @@ -5,5 +5,5 @@ def compose1[A, B, C](f: A => B, g: B => C): A ->{f, g} C = z => g(f(z)) def mapCompose[A](ps: List[(A => A, A => A)]): List[A ->{ps*} A] = - ps.map((x, y) => compose1(x, y)) // error // error + ps.map((x, y) => compose1(x, y)) // error diff --git a/tests/neg/leak-problem.scala b/tests/neg/leak-problem.scala index 5af66491e80b..f124c16635de 100644 --- a/tests/neg/leak-problem.scala +++ b/tests/neg/leak-problem.scala @@ -27,7 +27,7 @@ def test(): Unit = t1.read() val xsLambda2 = () => useBoxedAsync2(xs) - val _: () ->{xs*} Unit = xsLambda2 + val _: () ->{useBoxedAsync2, xs*} Unit = xsLambda2 // useBoxedAsync2 needed after adding addImplied widening val _: () -> Unit = xsLambda2 // error val f: Box[Async^] => Unit = (x: Box[Async^]) => useBoxedAsync(x) diff --git a/tests/pos-custom-args/captures/i20503.scala b/tests/pos-custom-args/captures/i20503.scala index c7aa2b02b81f..ec8764d3d6f9 100644 --- a/tests/pos-custom-args/captures/i20503.scala +++ b/tests/pos-custom-args/captures/i20503.scala @@ -8,10 +8,8 @@ class List[+A]: def nonEmpty: Boolean = ??? def runOps(ops: List[() => Unit]): Unit = - // See i20156, due to limitation in expressiveness of current system, - // we could map over the list of impure elements. OK with existentials. ops.foreach(op => op()) def main(): Unit = - val f: List[() => Unit] -> Unit = (ops: List[() => Unit]) => runOps(ops) // now ok - val _: List[() => Unit] -> Unit = runOps // now ok + val f: List[() => Unit] => Unit = (ops: List[() => Unit]) => runOps(ops) + val _: List[() => Unit] => Unit = runOps diff --git a/tests/pos-custom-args/captures/unsafe-unbox.scala b/tests/pos-custom-args/captures/unsafe-unbox.scala index 1c523490e19d..451d107c520d 100644 --- a/tests/pos-custom-args/captures/unsafe-unbox.scala +++ b/tests/pos-custom-args/captures/unsafe-unbox.scala @@ -1,6 +1,7 @@ import annotation.unchecked.uncheckedCaptures def test = - val finalizeActionsInit = collection.mutable.ListBuffer[(() => Unit) @uncheckedCaptures]() + val finalizeActionsInit: (collection.mutable.ListBuffer[(() => Unit) @uncheckedCaptures]^) @uncheckedCaptures + = collection.mutable.ListBuffer() var finalizeActions = finalizeActionsInit val action = finalizeActions.remove(0) diff --git a/tests/pos/cc-poly-source-capability.scala b/tests/pos/cc-poly-source-capability.scala index 9a21b2d5b802..1c45c87ba352 100644 --- a/tests/pos/cc-poly-source-capability.scala +++ b/tests/pos/cc-poly-source-capability.scala @@ -4,6 +4,12 @@ import caps.{CapSet, Capability} @experimental object Test: + class Set[T] extends Pure: // Define sets as `Pure` needed after adding addImplied widening + def +[T](x: T): Set[T] = ??? + + object Set: + def empty[T]: Set[T] = ??? + class Async extends Capability def listener(async: Async): Listener^{async} = ??? @@ -28,5 +34,8 @@ import caps.{CapSet, Capability} others.map(listener).foreach(src.register) val ls = src.allListeners val _: Set[Listener^{async1, others*}] = ls + // {ls, others*} would be added by addImplied here since sets are invariant + // But this is suppressed since Set is now declared to be pure. + diff --git a/tests/pos/cc-poly-source.scala b/tests/pos/cc-poly-source.scala index 939f1f682dc8..b70fe90e4254 100644 --- a/tests/pos/cc-poly-source.scala +++ b/tests/pos/cc-poly-source.scala @@ -4,6 +4,12 @@ import caps.{CapSet, Capability} @experimental object Test: + class Set[T] extends Pure: // Define sets as `Pure` needed after adding addImplied widening + def +[T](x: T): Set[T] = ??? + + object Set: + def empty[T]: Set[T] = ??? + class Label //extends Capability class Listener From 842240f358e30bb88cd821ce12fbf913f734b3d5 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 24 Oct 2024 23:41:22 +0200 Subject: [PATCH 17/17] More tests --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 4 ++++ .../captures/delayedRunops1.scala | 19 +++++++++++++++++ .../captures/delayedRunops5.scala | 21 +++++++++++++++++++ .../captures/delayedRunops6.scala | 17 +++++++++++++++ 4 files changed, 61 insertions(+) create mode 100644 tests/neg-custom-args/captures/delayedRunops1.scala create mode 100644 tests/neg-custom-args/captures/delayedRunops5.scala create mode 100644 tests/neg-custom-args/captures/delayedRunops6.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 51d1328d2ad6..f04c5ce18e41 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -821,6 +821,10 @@ object CaptureSet: // If this is the union of a constant and a variable, // propagate `elem` to the variable part to avoid slack // between the operands and the union. + // TODO: This means there's no back-propagation to the operands + // of a union of two variables. That could be a source of unsoundness. + // Altermative would be to be conservatibe and back-propagate to one + // of the operands arbitrarily or even to both of them. if res.isOK && (origin ne cs1) && (origin ne cs2) then if cs1.isConst then cs2.tryInclude(elem, origin) else if cs2.isConst then cs1.tryInclude(elem, origin) diff --git a/tests/neg-custom-args/captures/delayedRunops1.scala b/tests/neg-custom-args/captures/delayedRunops1.scala new file mode 100644 index 000000000000..17b34e1d01eb --- /dev/null +++ b/tests/neg-custom-args/captures/delayedRunops1.scala @@ -0,0 +1,19 @@ +import language.experimental.captureChecking + +def runOps(ops: List[() => Unit]): () ->{ops*} Unit = + () => ops.foreach(op => op()) + +def app[T, U](x: T, op: T => U): () ->{op} U = + () => op(x) + +def unsafeRunOps2(ops2: List[() => Unit]): () -> () -> Unit = + val x = app[List[() ->{ops2*} Unit], () ->{ops2*} Unit](ops2, runOps) // error + x + +def unsafeRunOps3(ops2: List[() => Unit]): () -> () -> Unit = + val x = app(ops2, runOps) // error + x + + + + diff --git a/tests/neg-custom-args/captures/delayedRunops5.scala b/tests/neg-custom-args/captures/delayedRunops5.scala new file mode 100644 index 000000000000..3b100ed8f179 --- /dev/null +++ b/tests/neg-custom-args/captures/delayedRunops5.scala @@ -0,0 +1,21 @@ +import language.experimental.captureChecking + +def runOps(ops: List[() => Unit]): Unit = + ops.foreach(op => op()) + +def app[T, U](x: T, op: T => U): () ->{op} U = + () => op(x) + +def test(c: Object^) = + + def unsafeRunOps1(ops: List[() ->{c} Unit]): () -> Unit = + app[List[() ->{c} Unit], Unit](ops, runOps) // !!! ok, but should be error + + def unsafeRunOps2(ops: List[() ->{c} Unit]): () -> Unit = + app(ops, runOps) // error + + () + + + + diff --git a/tests/neg-custom-args/captures/delayedRunops6.scala b/tests/neg-custom-args/captures/delayedRunops6.scala new file mode 100644 index 000000000000..ab9ac43a8835 --- /dev/null +++ b/tests/neg-custom-args/captures/delayedRunops6.scala @@ -0,0 +1,17 @@ +import language.experimental.captureChecking + +val runOps: [C^] -> () -> (ops: List[() ->{C^} Unit]) ->{C^} Unit = ??? + +def app[T, U](x: T, op: T => U): () ->{op} U = + () => op(x) + +def unsafeRunOps(ops: List[() => Unit]): () ->{} Unit = + app[List[() ->{ops*} Unit], Unit](ops, runOps()) // error + +def unsafeRunOps2(ops: List[() => Unit]): () ->{} Unit = + app(ops, runOps()) // error + +def test(c: Object^) = + def f = (ops: List[() ->{c} Unit]) => ops.foreach(_()) + val _: List[() ->{c} Unit] ->{c} Unit = f + ()