From 5b9f13b50383bdc50b856aef052a7ddc26da6ca3 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 14 Jun 2025 12:45:53 +0200 Subject: [PATCH] Flag nonsensical capturing types with pure parents as errors --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 4 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 33 ++++++---- .../src/dotty/tools/dotc/core/Types.scala | 12 ++-- .../src/scala/collection/Iterable.scala | 4 +- .../captures/exception-definitions.check | 2 +- tests/neg-custom-args/captures/i15116.check | 60 +++++++++---------- tests/neg-custom-args/captures/i15116.scala | 11 ++-- .../captures/nonsensical-for-pure.check | 4 ++ .../captures/nonsensical-for-pure.scala | 1 + .../captures/list-encoding.scala | 30 ++++++++++ .../captures/capt-depfun.scala | 19 +++--- .../captures/capt-depfun2.scala | 8 ++- tests/pos-custom-args/captures/capt2.scala | 2 +- .../pos-custom-args/captures/caseclass.scala | 7 ++- tests/pos-custom-args/captures/i16116.scala | 9 ++- tests/pos-custom-args/captures/i21868.scala | 8 ++- .../captures/list-encoding.scala | 24 -------- .../captures/trickyTrailingUpArrow.scala | 11 ++-- 18 files changed, 139 insertions(+), 110 deletions(-) create mode 100644 tests/neg-custom-args/captures/nonsensical-for-pure.check create mode 100644 tests/neg-custom-args/captures/nonsensical-for-pure.scala create mode 100644 tests/pending/pos-custom-args/captures/list-encoding.scala delete mode 100644 tests/pos-custom-args/captures/list-encoding.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 40fc6eb8f1de..faae83fd3456 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -280,9 +280,9 @@ extension (tp: Type) case tp: (TypeRef | AppliedType) => val sym = tp.typeSymbol if sym.isClass then sym.isPureClass - else tp.superType.isAlwaysPure + else !tp.superType.isAny && tp.superType.isAlwaysPure case tp: TypeProxy => - tp.superType.isAlwaysPure + !tp.superType.isAny && tp.superType.isAlwaysPure case tp: AndType => tp.tp1.isAlwaysPure || tp.tp2.isAlwaysPure case tp: OrType => diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 51ff6948a947..c8a3a023ea30 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -406,19 +406,26 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: CapturingType(fntpe, cs, boxed = false) else fntpe - /** Check that types extending SharedCapability don't have a `cap` in their capture set. - * TODO This is not enough. - * We need to also track that we cannot get exclusive capabilities in paths - * where some prefix derives from SharedCapability. Also, can we just - * exclude `cap`, or do we have to extend this to all exclusive capabilties? - * The problem is that we know what is exclusive in general only after capture - * checking, not before. + /** 1. Check that parents of capturing types are not pure. + * 2. Check that types extending SharedCapability don't have a `cap` in their capture set. + * TODO This is not enough. + * We need to also track that we cannot get exclusive capabilities in paths + * where some prefix derives from SharedCapability. Also, can we just + * exclude `cap`, or do we have to extend this to all exclusive capabilties? + * The problem is that we know what is exclusive in general only after capture + * checking, not before. */ - def checkSharedOK(tp: Type): tp.type = + def checkRetainsOK(tp: Type): tp.type = tp match - case CapturingType(parent, refs) - if refs.isUniversal && parent.derivesFromSharedCapability => - fail(em"$tp extends SharedCapability, so it cannot capture `cap`") + case CapturingType(parent, refs) => + if parent.isAlwaysPure && !tptToCheck.span.isZeroExtent then + // If tptToCheck is zero-extent it could be copied from an overridden + // method's result type. In that case, there's no point requiring + // an explicit result type in the override, the inherited capture set + // will be ignored anyway. + fail(em"$parent is a pure type, it makes no sense to add a capture set to it") + else if refs.isUniversal && parent.derivesFromSharedCapability then + fail(em"$tp extends SharedCapability, so it cannot capture `cap`") case _ => tp @@ -436,7 +443,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def innerApply(t: Type) = t match case t @ CapturingType(parent, refs) => - checkSharedOK: + checkRetainsOK: t.derivedCapturingType(stripImpliedCaptureSet(this(parent)), refs) case t @ AnnotatedType(parent, ann) => val parent1 = this(parent) @@ -445,7 +452,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if !tptToCheck.isEmpty then checkWellformedLater(parent2, ann.tree, tptToCheck) try - checkSharedOK: + checkRetainsOK: CapturingType(parent2, ann.tree.toCaptureSet) catch case ex: IllegalCaptureRef => if !tptToCheck.isEmpty then diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index ea4626ac684e..fbda30bd52a0 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1657,19 +1657,17 @@ object Types extends TypeUtils { NoType } - /** The iterator of underlying types as long as type is a TypeProxy. - * Useful for diagnostics + /** The iterator of underlying types staring with `this` and followed by + * repeatedly applying `f` as long as type is a TypeProxy. Useful for diagnostics. */ - def underlyingIterator(using Context): Iterator[Type] = new Iterator[Type] { + def iterate(f: TypeProxy => Type): Iterator[Type] = new Iterator[Type]: var current = Type.this var hasNext = true - def next() = { + def next() = val res = current hasNext = current.isInstanceOf[TypeProxy] - if (hasNext) current = current.asInstanceOf[TypeProxy].underlying + if hasNext then current = f(current.asInstanceOf[TypeProxy]) res - } - } /** A prefix-less refined this or a termRef to a new skolem symbol * that has the given type as info. diff --git a/scala2-library-cc/src/scala/collection/Iterable.scala b/scala2-library-cc/src/scala/collection/Iterable.scala index 6b6a03f88965..c5d10211e3ab 100644 --- a/scala2-library-cc/src/scala/collection/Iterable.scala +++ b/scala2-library-cc/src/scala/collection/Iterable.scala @@ -994,7 +994,7 @@ trait SortedSetFactoryDefaults[+A, +WithFilterCC[x] <: IterableOps[x, WithFilterCC, WithFilterCC[x]] with Set[x]] extends SortedSetOps[A @uncheckedVariance, CC, CC[A @uncheckedVariance]] { self: IterableOps[A, WithFilterCC, _] => - override protected def fromSpecific(coll: IterableOnce[A @uncheckedVariance]^): CC[A @uncheckedVariance]^{coll} = sortedIterableFactory.from(coll)(using ordering) + override protected def fromSpecific(coll: IterableOnce[A @uncheckedVariance]^): CC[A @uncheckedVariance] = sortedIterableFactory.from(coll)(using ordering) override protected def newSpecificBuilder: mutable.Builder[A @uncheckedVariance, CC[A @uncheckedVariance]] = sortedIterableFactory.newBuilder[A](using ordering) override def empty: CC[A @uncheckedVariance] = sortedIterableFactory.empty(using ordering) @@ -1050,7 +1050,7 @@ trait SortedMapFactoryDefaults[K, +V, self: IterableOps[(K, V), WithFilterCC, _] => override def empty: CC[K, V @uncheckedVariance] = sortedMapFactory.empty(using ordering) - override protected def fromSpecific(coll: IterableOnce[(K, V @uncheckedVariance)]^): CC[K, V @uncheckedVariance]^{coll} = sortedMapFactory.from(coll)(using ordering) + override protected def fromSpecific(coll: IterableOnce[(K, V @uncheckedVariance)]^): CC[K, V @uncheckedVariance] = sortedMapFactory.from(coll)(using ordering) override protected def newSpecificBuilder: mutable.Builder[(K, V @uncheckedVariance), CC[K, V @uncheckedVariance]] = sortedMapFactory.newBuilder[K, V](using ordering) override def withFilter(p: ((K, V)) => Boolean): collection.SortedMapOps.WithFilter[K, V, WithFilterCC, UnsortedCC, CC]^{p} = diff --git a/tests/neg-custom-args/captures/exception-definitions.check b/tests/neg-custom-args/captures/exception-definitions.check index 67f0f3b72cbb..be5ea4304bf5 100644 --- a/tests/neg-custom-args/captures/exception-definitions.check +++ b/tests/neg-custom-args/captures/exception-definitions.check @@ -1,7 +1,7 @@ -- Error: tests/neg-custom-args/captures/exception-definitions.scala:3:8 ----------------------------------------------- 3 | self: Err^ => // error | ^^^^ - |reference cap captured by this self type is not included in the allowed capture set {} of pure base class class Throwable + | Err is a pure type, it makes no sense to add a capture set to it -- Error: tests/neg-custom-args/captures/exception-definitions.scala:7:12 ---------------------------------------------- 7 | val x = c // error | ^ diff --git a/tests/neg-custom-args/captures/i15116.check b/tests/neg-custom-args/captures/i15116.check index 50784d393162..9effc46bd6c1 100644 --- a/tests/neg-custom-args/captures/i15116.check +++ b/tests/neg-custom-args/captures/i15116.check @@ -1,7 +1,7 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:3:13 ---------------------------------------- -3 | val x = Foo(m) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:4:13 ---------------------------------------- +4 | val x = Foo(m) // error | ^^^^^^ - | Found: Foo{val m: (Bar.this.m² : String^)}^{Bar.this.m²} + | Found: Foo{val m: (Bar.this.m² : STR^)}^{Bar.this.m²} | Required: Foo | | where: ^ refers to a fresh root capability in the type of value m² @@ -12,14 +12,14 @@ | 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: (Bar.this.m : String^)}^{Bar.this.m} + | The new inferred type Foo{val m: (Bar.this.m : STR^)}^{Bar.this.m} | must conform to this type. | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:5:13 ---------------------------------------- -5 | val x = Foo(m) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:6:13 ---------------------------------------- +6 | val x = Foo(m) // error | ^^^^^^ - | Found: Foo{val m: (Baz.this.m² : String^)}^{Baz.this.m²} + | Found: Foo{val m: (Baz.this.m² : STR^)}^{Baz.this.m²} | Required: Foo | | where: ^ refers to a fresh root capability in the type of value m² @@ -30,14 +30,14 @@ | 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: (Baz.this.m : String^)}^{Baz.this.m} + | The new inferred type Foo{val m: (Baz.this.m : STR^)}^{Baz.this.m} | must conform to this type. | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:7:13 ---------------------------------------- -7 | val x = Foo(m) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:8:13 ---------------------------------------- +8 | val x = Foo(m) // error | ^^^^^^ - | Found: Foo{val m: (Bar1.this.m² : String^)}^{Bar1.this.m²} + | Found: Foo{val m: (Bar1.this.m² : STR^)}^{Bar1.this.m²} | Required: Foo | | where: ^ refers to a fresh root capability in the type of value m² @@ -48,25 +48,25 @@ | 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: (Bar1.this.m : String^)}^{Bar1.this.m} - | must conform to this type. - | - | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:9:13 ---------------------------------------- -9 | val x = Foo(m) // error - | ^^^^^^ - | Found: Foo{val m: (Baz2.this.m² : String^)}^{Baz2.this.m²} - | Required: Foo - | - | where: ^ refers to a fresh root capability in the type of value m² - | m is a value in class Foo - | m² is a value in trait Baz2 - | - | - | 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: (Baz2.this.m : String^)}^{Baz2.this.m} + | The new inferred type Foo{val m: (Bar1.this.m : STR^)}^{Bar1.this.m} | must conform to this type. | | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:10:13 --------------------------------------- +10 | val x = Foo(m) // error + | ^^^^^^ + | Found: Foo{val m: (Baz2.this.m² : STR^)}^{Baz2.this.m²} + | Required: Foo + | + | where: ^ refers to a fresh root capability in the type of value m² + | m is a value in class Foo + | m² is a value in trait Baz2 + | + | + | 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: (Baz2.this.m : STR^)}^{Baz2.this.m} + | must conform to this type. + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i15116.scala b/tests/neg-custom-args/captures/i15116.scala index c4dc6c88d56c..fb9fbfeb8aea 100644 --- a/tests/neg-custom-args/captures/i15116.scala +++ b/tests/neg-custom-args/captures/i15116.scala @@ -1,9 +1,10 @@ -class Foo(m: String^) -class Bar(val m: String^): +class STR +class Foo(m: STR^) +class Bar(val m: STR^): val x = Foo(m) // error -trait Baz(val m: String^): +trait Baz(val m: STR^): val x = Foo(m) // error -class Bar1(m: String^): +class Bar1(m: STR^): val x = Foo(m) // error -trait Baz2(m: String^): +trait Baz2(m: STR^): val x = Foo(m) // error diff --git a/tests/neg-custom-args/captures/nonsensical-for-pure.check b/tests/neg-custom-args/captures/nonsensical-for-pure.check new file mode 100644 index 000000000000..6860446c4177 --- /dev/null +++ b/tests/neg-custom-args/captures/nonsensical-for-pure.check @@ -0,0 +1,4 @@ +-- Error: tests/neg-custom-args/captures/nonsensical-for-pure.scala:1:7 ------------------------------------------------ +1 |val x: Int^ = 2 // error + | ^^^^ + | Int is a pure type, it makes no sense to add a capture set to it diff --git a/tests/neg-custom-args/captures/nonsensical-for-pure.scala b/tests/neg-custom-args/captures/nonsensical-for-pure.scala new file mode 100644 index 000000000000..ffdd9c28e7bf --- /dev/null +++ b/tests/neg-custom-args/captures/nonsensical-for-pure.scala @@ -0,0 +1 @@ +val x: Int^ = 2 // error diff --git a/tests/pending/pos-custom-args/captures/list-encoding.scala b/tests/pending/pos-custom-args/captures/list-encoding.scala new file mode 100644 index 000000000000..38e3affa8671 --- /dev/null +++ b/tests/pending/pos-custom-args/captures/list-encoding.scala @@ -0,0 +1,30 @@ +package listEncoding +import annotation.retains + +class Cap + +type Op[T, C] = + T => C => C + + +class STR // used to be pure class String but then the errors don't show up. + +type List[T] = + [C] -> (op: Op[T, C]) -> (s: C) ->{op} C + +def nil[T]: List[T] = + [C] => (op: Op[T, C]) => (s: C) => s + +def cons[T](hd: T, tl: List[T]): List[T] = + [C] => (op: Op[T, C]) => (s: C) => op(hd)(tl(op)(s)) + +def foo(c: Cap^) = + def f(x: STR @retains[c.type], y: STR @retains[c.type]) = + cons(x, cons(y, nil)) // error, should this work? + def f_explicit(x: STR @retains[c.type], y: STR @retains[c.type]) + : [C] => (op: STR^{x, y} => C => C) -> (s: C) ->{op} C = + cons(x, cons(y, nil)) // error, should this work? + def g(x: STR @retains[c.type], y: Any) = + cons(x, cons(y, nil)) + def h(x: STR, y: Any @retains[c.type]) = + cons(x, cons(y, nil)) diff --git a/tests/pos-custom-args/captures/capt-depfun.scala b/tests/pos-custom-args/captures/capt-depfun.scala index bf4fb7da2cd4..fb6866459aba 100644 --- a/tests/pos-custom-args/captures/capt-depfun.scala +++ b/tests/pos-custom-args/captures/capt-depfun.scala @@ -1,21 +1,24 @@ import annotation.retains +import caps.consume class C type Cap = C @retains[caps.cap.type] -type T = (x: Cap) -> String @retains[x.type] +type T = (x: Cap) -> STR @retains[x.type] type ID[X] = X -val aa: ((x: Cap) -> String @retains[x.type]) = (x: Cap) => "" +class STR(s: String) -def f(y: Cap, z: Cap): String @retains[caps.cap.type] = - val a: ((x: Cap) -> String @retains[x.type]) = (x: Cap) => "" +val aa: ((x: Cap) -> STR @retains[x.type]) = (x: Cap) => STR("") + +def f(@consume y: Cap, z: Cap): STR @retains[caps.cap.type] = + val a: ((x: Cap) -> STR @retains[x.type]) = (x: Cap) => STR("") val b = a(y) - val c: String @retains[y.type] = b + val c: STR @retains[y.type] = b def g(): C @retains[y.type | z.type] = ??? val d = a(g()) - val ac: ((x: Cap) -> ID[String @retains[x.type] -> String @retains[x.type]]) = ??? - val bc: String^{y} -> String^{y} = ac(y) - val dc: String -> String^{y, z} = ac(g()) + val ac: ((x: Cap) -> ID[STR @retains[x.type] -> STR @retains[x.type]]) = ??? + val bc: STR^{y} -> STR^{y} = ac(y) + val dc: STR -> STR^{y, z} = ac(g()) c diff --git a/tests/pos-custom-args/captures/capt-depfun2.scala b/tests/pos-custom-args/captures/capt-depfun2.scala index 50b0fa9d3d99..101ea6bb94e1 100644 --- a/tests/pos-custom-args/captures/capt-depfun2.scala +++ b/tests/pos-custom-args/captures/capt-depfun2.scala @@ -1,9 +1,11 @@ import annotation.retains +import caps.consume class C type Cap = C @retains[caps.cap.type] -def f(y: Cap, z: Cap) = +class STR +def f(@consume y: Cap, @consume z: Cap) = def g(): C @retains[y.type | z.type] = ??? - val ac: ((x: Cap) -> Array[String @retains[x.type]]) = ??? - val dc: Array[? >: String <: String]^{y, z} = ac(g()) // needs to be inferred + val ac: ((x: Cap) -> Array[STR @retains[x.type]]) = ??? + val dc: Array[? >: STR <: STR^]^{y, z} = ac(g()) // needs to be inferred val ec = ac(y) diff --git a/tests/pos-custom-args/captures/capt2.scala b/tests/pos-custom-args/captures/capt2.scala index 3bfb1c70fbe8..7690b892d120 100644 --- a/tests/pos-custom-args/captures/capt2.scala +++ b/tests/pos-custom-args/captures/capt2.scala @@ -3,7 +3,7 @@ class C type Cap = C @retains[caps.cap.type] def test1() = - val y: String^ = "" + val y: C^ = C() def x: Object @retains[y.type] = y def test2() = diff --git a/tests/pos-custom-args/captures/caseclass.scala b/tests/pos-custom-args/captures/caseclass.scala index fe7e02b1b6c2..bae55a82c3bc 100644 --- a/tests/pos-custom-args/captures/caseclass.scala +++ b/tests/pos-custom-args/captures/caseclass.scala @@ -1,12 +1,13 @@ class C extends caps.Capability +class STR(s: String) object test1: - case class Ref(x: String^) + case class Ref(x: STR^) def test(c: C) = - val x1 = Ref("hello") + val x1 = Ref(STR("hello")) val y = x1 match case Ref(z) => z - val yc: String = y + val yc: STR = y object test2: case class Ref(x: () => Unit) diff --git a/tests/pos-custom-args/captures/i16116.scala b/tests/pos-custom-args/captures/i16116.scala index fdc386ac40e1..a103b9a740e2 100644 --- a/tests/pos-custom-args/captures/i16116.scala +++ b/tests/pos-custom-args/captures/i16116.scala @@ -26,12 +26,15 @@ object Test { def apply[A](expr: (CpsTransform[F], C) ?=> A): F[A] = ??? } - def asyncPlus[F[_]](a:Int, b:F[Int])(using cps: CpsTransform[F]): Int^{ cps } = - a + (cps.await(b).asInstanceOf[Int]) + class C(x: Int): + def +(that: C): C = ??? + + def asyncPlus[F[_]](a:C, b:F[C])(using cps: CpsTransform[F]): C^{ cps } = + a + (cps.await(b).asInstanceOf[C]) def testExample1Future(): Unit = val fr = cpsAsync[Future] { - val y = asyncPlus(1,Future successful 2).asInstanceOf[Int] + val y = asyncPlus(C(1),Future successful C(2)).asInstanceOf[Int] y+1 } diff --git a/tests/pos-custom-args/captures/i21868.scala b/tests/pos-custom-args/captures/i21868.scala index 560177c87d73..b0d83efc03ca 100644 --- a/tests/pos-custom-args/captures/i21868.scala +++ b/tests/pos-custom-args/captures/i21868.scala @@ -1,14 +1,16 @@ import language.experimental.captureChecking import caps.* +class U + trait AbstractWrong: type C^ <: CapSet // no longer an error, the lower bound becomes CapSet now - def f(): Unit^{C} + def f(): U^{C} trait Abstract1: type C^ >: CapSet <: CapSet^ - def f(): Unit^{C} + def f(): U^{C} trait Abstract2: type C^ <: {cap} - def f(): Unit^{C} \ No newline at end of file + def f(): U^{C} \ No newline at end of file diff --git a/tests/pos-custom-args/captures/list-encoding.scala b/tests/pos-custom-args/captures/list-encoding.scala deleted file mode 100644 index d427d1ae3f41..000000000000 --- a/tests/pos-custom-args/captures/list-encoding.scala +++ /dev/null @@ -1,24 +0,0 @@ -package listEncoding -import annotation.retains - -class Cap - -type Op[T, C] = - T => C => C - -type List[T] = - [C] -> (op: Op[T, C]) -> (s: C) ->{op} C - -def nil[T]: List[T] = - [C] => (op: Op[T, C]) => (s: C) => s - -def cons[T](hd: T, tl: List[T]): List[T] = - [C] => (op: Op[T, C]) => (s: C) => op(hd)(tl(op)(s)) - -def foo(c: Cap^) = - def f(x: String @retains[c.type], y: String @retains[c.type]) = - cons(x, cons(y, nil)) - def g(x: String @retains[c.type], y: Any) = - cons(x, cons(y, nil)) - def h(x: String, y: Any @retains[c.type]) = - cons(x, cons(y, nil)) diff --git a/tests/pos-custom-args/captures/trickyTrailingUpArrow.scala b/tests/pos-custom-args/captures/trickyTrailingUpArrow.scala index 71b663de5354..1ca4403a54b7 100644 --- a/tests/pos-custom-args/captures/trickyTrailingUpArrow.scala +++ b/tests/pos-custom-args/captures/trickyTrailingUpArrow.scala @@ -1,9 +1,10 @@ -object Test: + class STR + object Test: var x = 0 - type FreshContext = String^ + type FreshContext = STR^ x += 1 - inline def ctx(using c: String) = c + inline def ctx(using c: STR) = c - val y: String^ -> Unit = ??? - val z: String^ ?-> Unit = ??? + val y: STR^ -> Unit = ??? + val z: STR^ ?-> Unit = ???