Repository: scalameta/scalameta (scala)

Issue:
Support capture checking related syntax
Scalameta should support syntax related to capture checking.
These changes include:
- syntax for indicating captures `tpe^` and `tpe^{...}`
- syntax for capturing functions `tpe ->^{...} tpe`

Links:
- implementation PR: https://github.com/scala/scala3/pull/15877/files#diff-7b0af84ac42fe737a4f3700b2ce9838c02f7bc1f7d4d266f44b07c6b33199abb

Pull request description:
Fixes #3996.

Patch:
diff --git a/scalameta/dialects/shared/src/main/scala/scala/meta/Dialect.scala b/scalameta/dialects/shared/src/main/scala/scala/meta/Dialect.scala
index 376c203082..a0abd94d65 100644
--- a/scalameta/dialects/shared/src/main/scala/scala/meta/Dialect.scala
+++ b/scalameta/dialects/shared/src/main/scala/scala/meta/Dialect.scala
@@ -169,6 +169,8 @@ final class Dialect private[meta] (
     val allowParameterTypeConversions: Boolean,
     // https://dotty.epfl.ch/docs/reference/experimental/purefuns.html
     val allowPureFunctions: Boolean,
+    // https://dotty.epfl.ch/docs/reference/experimental/cc.html
+    val allowCaptureChecking: Boolean,
     // https://docs3.scala-lang.org/sips/sips/typeclasses-syntax.html?
     val allowImprovedTypeClassesSyntax: Boolean
 ) extends Product with Serializable {
@@ -269,6 +271,7 @@ final class Dialect private[meta] (
     allowTrackedParameters = false,
     allowParameterTypeConversions = false,
     allowPureFunctions = false,
+    allowCaptureChecking = false,
     allowImprovedTypeClassesSyntax = false
     // NOTE(olafur): declare the default value for new fields above this comment.
   )
@@ -415,6 +418,9 @@ final class Dialect private[meta] (
 
   def withAllowPureFunctions(newValue: Boolean): Dialect = privateCopy(allowPureFunctions = newValue)
 
+  def withAllowCaptureChecking(newValue: Boolean): Dialect =
+    privateCopy(allowCaptureChecking = newValue)
+
   def withAllowImprovedTypeClassesSyntax(newValue: Boolean): Dialect =
     privateCopy(allowImprovedTypeClassesSyntax = newValue)
 
@@ -486,6 +492,7 @@ final class Dialect private[meta] (
       allowTrackedParameters: Boolean = this.allowTrackedParameters,
       allowParameterTypeConversions: Boolean = this.allowParameterTypeConversions,
       allowPureFunctions: Boolean = this.allowPureFunctions,
+      allowCaptureChecking: Boolean = this.allowCaptureChecking,
       allowImprovedTypeClassesSyntax: Boolean = this.allowImprovedTypeClassesSyntax
       // NOTE(olafur): add the next parameter above this comment.
   ): Dialect = {
@@ -552,6 +559,7 @@ final class Dialect private[meta] (
       allowTrackedParameters = allowTrackedParameters,
       allowParameterTypeConversions = allowParameterTypeConversions,
       allowPureFunctions = allowPureFunctions,
+      allowCaptureChecking = allowCaptureChecking,
       allowImprovedTypeClassesSyntax = allowImprovedTypeClassesSyntax
     )
     if (equivalent) return this // RETURN!
@@ -621,6 +629,7 @@ final class Dialect private[meta] (
       allowTrackedParameters = allowTrackedParameters,
       allowParameterTypeConversions = allowParameterTypeConversions,
       allowPureFunctions = allowPureFunctions,
+      allowCaptureChecking = allowCaptureChecking,
       allowImprovedTypeClassesSyntax = allowImprovedTypeClassesSyntax,
       // NOTE(olafur): add the next argument above this comment.
       unquoteType = unquoteType,
@@ -712,6 +721,7 @@ final class Dialect private[meta] (
       allowTrackedParameters: Boolean,
       allowParameterTypeConversions: Boolean,
       allowPureFunctions: Boolean,
+      allowCaptureChecking: Boolean,
       allowImprovedTypeClassesSyntax: Boolean
   ): Boolean =
     // do not include deprecated values in this comparison
@@ -768,6 +778,7 @@ final class Dialect private[meta] (
       this.allowTrackedParameters == allowTrackedParameters &&
       this.allowParameterTypeConversions == allowParameterTypeConversions &&
       this.allowPureFunctions == allowPureFunctions &&
+      this.allowCaptureChecking == allowCaptureChecking &&
       this.allowImprovedTypeClassesSyntax == allowImprovedTypeClassesSyntax
 
   @inline
@@ -834,6 +845,7 @@ final class Dialect private[meta] (
       allowTrackedParameters = that.allowTrackedParameters,
       allowParameterTypeConversions = that.allowParameterTypeConversions,
       allowPureFunctions = that.allowPureFunctions,
+      allowCaptureChecking = that.allowCaptureChecking,
       allowImprovedTypeClassesSyntax = that.allowImprovedTypeClassesSyntax
     )
 
diff --git a/scalameta/dialects/shared/src/main/scala/scala/meta/dialects/package.scala b/scalameta/dialects/shared/src/main/scala/scala/meta/dialects/package.scala
index 3109098148..86d8dc416b 100644
--- a/scalameta/dialects/shared/src/main/scala/scala/meta/dialects/package.scala
+++ b/scalameta/dialects/shared/src/main/scala/scala/meta/dialects/package.scala
@@ -112,7 +112,7 @@ package object dialects {
   implicit val Scala3: Dialect = Scala36
 
   implicit val Scala3Future: Dialect = Scala3.withAllowUnderscoreAsTypePlaceholder(true)
-    .withAllowTrackedParameters(true).withAllowPureFunctions(true)
+    .withAllowTrackedParameters(true).withAllowPureFunctions(true).withAllowCaptureChecking(true)
 
   @deprecated("Use Scala3 instead", "4.4.2")
   implicit val Dotty: Dialect = Scala3
diff --git a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScalametaParser.scala b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScalametaParser.scala
index 14f72630d0..71d69c86f3 100644
--- a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScalametaParser.scala
+++ b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScalametaParser.scala
@@ -692,29 +692,36 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
         }
 
       val openParenPos = currIndex
-      val ts = inParensOr(commaSeparated(paramOrType(List.newBuilder[Mod])))(Nil)
       // NOTE: can't have this, because otherwise we run into #312
       // newLineOptWhenFollowedBy[LeftParen]
 
-      var hasTypes = false
-      var hasParams = false
-      ts.foreach {
-        case _: Quasi =>
-        case _: Type.TypedParam => hasParams = true
-        case _ => hasTypes = true
-      }
-      if (hasTypes && hasParams)
-        syntaxError("can't mix function type and dependent function type syntaxes", at = currToken)
-      if (hasParams && !dialect.allowDependentFunctionTypes)
-        syntaxError("dependent function types are not supported", at = currToken)
-      if (!hasTypes && !hasImplicits && at[LeftParen]) {
-        val message = "can't have multiple parameter lists in function types"
-        syntaxError(message, at = currToken)
+      def makeTuple(ts: List[Type]) = makeTupleType(openParenPos, ts)
+
+      val ts = {
+        val ts = inParensOr(commaSeparated(paramOrType(List.newBuilder[Mod])))(Nil)
+
+        var hasTypes = false
+        var hasParams = false
+        ts.foreach {
+          case _: Quasi =>
+          case _: Type.TypedParam => hasParams = true
+          case _ => hasTypes = true
+        }
+        if (hasTypes && hasParams)
+          syntaxError("can't mix function type and dependent function type syntaxes", at = currToken)
+        if (hasParams && !dialect.allowDependentFunctionTypes)
+          syntaxError("dependent function types are not supported", at = currToken)
+        if (!hasTypes && !hasImplicits && at[LeftParen]) {
+          val message = "can't have multiple parameter lists in function types"
+          syntaxError(message, at = currToken)
+        }
+
+        withTypeCapturesOpt(openParenPos, needCaret = true)(makeTuple(ts)).fold(ts)(_ :: Nil)
       }
 
       def maybeFunc = getAfterOptNewLine(typeFuncOnArrowOpt(openParenPos, ts))
       (if (allowFunctionType) maybeFunc else None).getOrElse {
-        val simple = simpleTypeRest(makeTupleType(openParenPos, ts), openParenPos)
+        val simple = simpleTypeRest(makeTuple(ts), openParenPos)
         val compound = compoundTypeRest(annotTypeRest(simple, openParenPos), openParenPos)
         infixTypeRest(compound) match {
           case `compound` => compound
@@ -778,7 +785,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
         case soft.KwPureFunctionArrow() =>
           val startPos = currIndex
           next()
-          autoEndPos(startPos)(Type.PureByName(byNameParamValueType))
+          withTypeCaptures(startPos)(autoEndPos(startPos)(Type.PureByName(byNameParamValueType)))
         case _ => paramValueType(allowRepeated = true)
       }
     }
@@ -811,12 +818,36 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
       }
       else typ()
 
-    private def typeFuncOnArrow(paramPos: Int, params: List[Type])(
+    private def typeCaptures(needCaret: Boolean, allowCaptures: Boolean): Option[List[Term.Ref]] =
+      if (allowCaptures && dialect.allowCaptureChecking) {
+        def captures(): Option[List[Term.Ref]] = Some(commaSeparated(path()))
+        if (needCaret) currToken match {
+          case Token.Ident("^") => next(); inBracesOr(captures())(Some(Nil))
+          case _ => None
+        }
+        else inBracesOr(captures())(None)
+      } else None
+
+    private def withTypeCapturesOpt(startPos: Int, needCaret: Boolean, allowCaptures: Boolean = true)(
+        capturedType: => Type
+    ): Option[Type] = typeCaptures(needCaret = needCaret, allowCaptures = allowCaptures)
+      .map(captures => autoEndPos(startPos)(Type.Capturing(capturedType, captures)))
+
+    private def withTypeCaptures(
+        startPos: Int,
+        needCaret: Boolean = false,
+        allowCaptures: Boolean = true
+    )(capturedType: => Type): Type =
+      withTypeCapturesOpt(startPos, needCaret = needCaret, allowCaptures = allowCaptures)(
+        capturedType
+      ).getOrElse(capturedType)
+
+    private def typeFuncOnArrow(paramPos: Int, params: List[Type], allowCaptures: Boolean = false)(
         ctor: (Type.FuncParamClause, Type) => Type
     ): Type = {
       val funcParams = autoEndPos(paramPos)(params.reduceWith(Type.FuncParamClause.apply))
       next()
-      ctor(funcParams, typeIndentedOpt())
+      withTypeCaptures(paramPos, allowCaptures = allowCaptures)(ctor(funcParams, typeIndentedOpt()))
     }
 
     private def typeFuncOnArrowOpt(paramPos: Int, params: List[Type]): Option[Type] =
@@ -825,9 +856,9 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
         case _: ContextArrow => Some(typeFuncOnArrow(paramPos, params)(Type.ContextFunction(_, _)))
         case t: Ident => t.text match {
             case soft.KwPureFunctionArrow() =>
-              Some(typeFuncOnArrow(paramPos, params)(Type.PureFunction(_, _)))
+              Some(typeFuncOnArrow(paramPos, params, allowCaptures = true)(Type.PureFunction(_, _)))
             case soft.KwPureContextFunctionArrow() =>
-              Some(typeFuncOnArrow(paramPos, params)(Type.PureContextFunction(_, _)))
+              Some(typeFuncOnArrow(paramPos, params, allowCaptures = true)(Type.PureContextFunction(_, _)))
             case _ => None
           }
         case _ => None
@@ -1026,7 +1057,8 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
           }
         case _ => pathSimpleType()
       }
-      simpleTypeRest(autoEndPosOpt(startPos)(res), startPos)
+      val rest = simpleTypeRest(autoEndPosOpt(startPos)(res), startPos)
+      withTypeCaptures(startPos, needCaret = true)(rest)
     }
 
     @tailrec
@@ -1087,6 +1119,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
         case t: Type.AnonymousParam => Type.AnonymousParam(t.variant)
         case t: Type.Placeholder => Type.Placeholder(t.bounds)
         case t: Type.Block => Type.Block(t.typeDefs, copyType(t.tpe))
+        case t: Type.Capturing => Type.Capturing(convertType(t.tpe), t.caps)
         case tpe: Lit => tpe
       })
       val t: Type = PatternTypeContext.within {
diff --git a/scalameta/trees/shared/src/main/scala/scala/meta/Trees.scala b/scalameta/trees/shared/src/main/scala/scala/meta/Trees.scala
index 67192866a5..d90513464a 100644
--- a/scalameta/trees/shared/src/main/scala/scala/meta/Trees.scala
+++ b/scalameta/trees/shared/src/main/scala/scala/meta/Trees.scala
@@ -744,6 +744,12 @@ object Type {
     final def stats: List[Tree] = typeDefs
   }
 
+  @ast
+  class Capturing(
+      tpe: Type,
+      caps: List[Term.Ref] // [cap]tures or [cap]abilities
+  ) extends Type
+
   def fresh(): Type.Name = fresh("fresh")
   def fresh(prefix: String): Type.Name = Type.Name(prefix + Fresh.nextId())
 }
diff --git a/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeSyntax.scala b/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeSyntax.scala
index 7542ab3541..ea8ced6b19 100644
--- a/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeSyntax.scala
+++ b/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeSyntax.scala
@@ -704,6 +704,10 @@ object TreeSyntax {
         val cbounds = r(t.cbounds.map(s(kw(":"), " ", _)))
         s(w(mods, " "), variance, t.name, t.tparamClause, tbounds, vbounds, cbounds)
       case t: Type.Block => s(w(r(t.typeDefs, "; "), "; "), t.tpe)
+      case t: Type.Capturing => t.tpe match {
+          case tpe: Type.FunctionLikeType => s(tpe)
+          case tpe => s(tpe, printCaps(t.caps, "^"))
+        }
 
       // Pat
       case t: Pat.Var => m(SimplePattern, printMaybeBackquoted(t.name))
@@ -1059,14 +1063,26 @@ object TreeSyntax {
       builder.result()
     }
 
-    private def printFunctionLikeType(t: Type, params: Show.Result, arrow: String): Show.Result =
-      m(Typ, s(w(params, " "), kw(arrow), " ", p(Typ, t)))
+    private def printCaps(captures: List[Term.Ref], prefix: String = ""): Show.Result =
+      s(prefix, w("{", r(captures, ", "), "}"))
+
+    private def printFunctionLikeType(
+        t: Type.FunctionLikeType,
+        params: Show.Result,
+        arrow: String
+    ): Show.Result = {
+      val caps = t.parent match {
+        case Some(p: Type.Capturing) => printCaps(p.caps)
+        case _ => Show.None
+      }
+      m(Typ, s(w(params, " "), kw(arrow), caps, " ", p(Typ, t.res)))
+    }
 
     private def printFunctionType(t: Type.ParamFunctionType, arrow: String): Show.Result =
-      printFunctionLikeType(t.res, s(t.paramClause), arrow)
+      printFunctionLikeType(t, s(t.paramClause), arrow)
 
     private def printByNameType(t: Type.ByNameType, arrow: String): Show.Result =
-      printFunctionLikeType(t.tpe, s(), arrow)
+      printFunctionLikeType(t, s(), arrow)
 
     private def printMaybeBackquoted(name: Name) = printBackquoted(name, guessIsBackquoted(name))
     private def printBackquoted(name: Name, isBackquoted: Boolean) =
diff --git a/scalameta/trees/shared/src/main/scala/scala/meta/internal/trees/ParentChecks.scala b/scalameta/trees/shared/src/main/scala/scala/meta/internal/trees/ParentChecks.scala
index 1d4690b914..1f468046a5 100644
--- a/scalameta/trees/shared/src/main/scala/scala/meta/internal/trees/ParentChecks.scala
+++ b/scalameta/trees/shared/src/main/scala/scala/meta/internal/trees/ParentChecks.scala
@@ -41,6 +41,7 @@ object ParentChecks {
   private def typeArgument(tree: Type, parent: Tree, destination: String): Boolean = parent match {
     case _: Term.Param => destination == "decltpe"
     case _: Type.FuncParamClause => true
+    case _: Type.Capturing => true
     case _: Type.ByNameType => !tree.is[Type.ByNameType] && destination == "tpe"
     case _: Type.Repeated => !tree.is[Type.Repeated] && destination == "tpe"
     case _: Type.FunctionArg => !tree.is[Type.FunctionArg] && destination == "tpe"


Test patch:
diff --git a/tests/jvm/src/test/scala-2.13/scala/meta/tests/api/SurfaceSuite.scala b/tests/jvm/src/test/scala-2.13/scala/meta/tests/api/SurfaceSuite.scala
index 327144f7ce..6e13eddb63 100644
--- a/tests/jvm/src/test/scala-2.13/scala/meta/tests/api/SurfaceSuite.scala
+++ b/tests/jvm/src/test/scala-2.13/scala/meta/tests/api/SurfaceSuite.scala
@@ -495,6 +495,7 @@ class SurfaceSuite extends TreeSuiteBase {
          |scala.meta.Type.BoundsAlias
          |scala.meta.Type.ByName
          |scala.meta.Type.ByNameType
+         |scala.meta.Type.Capturing
          |scala.meta.Type.CasesBlock
          |scala.meta.Type.ContextFunction
          |scala.meta.Type.Existential
diff --git a/tests/jvm/src/test/scala-2.13/scala/meta/tests/trees/ReflectionSuite.scala b/tests/jvm/src/test/scala-2.13/scala/meta/tests/trees/ReflectionSuite.scala
index 805ef114ea..0a9954c50e 100644
--- a/tests/jvm/src/test/scala-2.13/scala/meta/tests/trees/ReflectionSuite.scala
+++ b/tests/jvm/src/test/scala-2.13/scala/meta/tests/trees/ReflectionSuite.scala
@@ -24,7 +24,7 @@ class ReflectionSuite extends TreeSuiteBase {
     val sym = symbolOf[scala.meta.Tree]
     assert(sym.isRoot)
     val root = sym.asRoot
-    assertEquals((root.allBranches.length, root.allLeafs.length), (67, 457))
+    assertEquals((root.allBranches.length, root.allLeafs.length), (67, 459))
   }
 
   test("If") {
@@ -99,6 +99,7 @@ class ReflectionSuite extends TreeSuiteBase {
          |List[scala.meta.Stat]
          |List[scala.meta.Term.Name]
          |List[scala.meta.Term.Param]
+         |List[scala.meta.Term.Ref]
          |List[scala.meta.Term]
          |List[scala.meta.Type.Param]
          |List[scala.meta.TypeCase]
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/NewFunctionsSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/NewFunctionsSuite.scala
index 55d62e4c89..e0d525eb40 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/NewFunctionsSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/NewFunctionsSuite.scala
@@ -802,6 +802,21 @@ class NewFunctionsSuite extends BaseDottySuite {
     runTestAssert[Stat](code, layout)(tree)
   }
 
+  test("#3996 pure context functions: 4") {
+    implicit val dialect: Dialect = dialects.Scala3Future
+    val code = "def map[T <: (A ?->{a, c} B)](f: T): A ?-> B = ???"
+    val layout = "def map[T <: A ?->{a, c} B](f: T): A ?-> B = ???"
+    val tree = Defn.Def(
+      Nil,
+      "map",
+      List(pparam("T", hiBound(Type.Capturing(purectxfunc(List("A"), "B"), List("a", "c"))))),
+      List(List(tparam("f", "T"))),
+      Some(purectxfunc(List("A"), "B")),
+      "???"
+    )
+    runTestAssert[Stat](code, layout)(tree)
+  }
+
   test("#3996 pure by-name: 1") {
     implicit val dialect: Dialect = dialects.Scala3Future
     val code = "def func(f: -> B): Unit"
@@ -810,4 +825,18 @@ class NewFunctionsSuite extends BaseDottySuite {
     runTestAssert[Stat](code, layout)(tree)
   }
 
+  test("#3996 pure by-name: 2 with capturing") {
+    implicit val dialect: Dialect = dialects.Scala3Future
+    val code = "def func(f: ->{a, b, c} B): Unit"
+    val layout = "def func(f: ->{a, b, c} B): Unit"
+    val tree = Decl.Def(
+      Nil,
+      "func",
+      Nil,
+      List(List(tparam("f", Type.Capturing(Type.PureByName("B"), List("a", "b", "c"))))),
+      "Unit"
+    )
+    runTestAssert[Stat](code, layout)(tree)
+  }
+
 }
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/TypeSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/TypeSuite.scala
index 0f45c4b886..bfae79da30 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/TypeSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/TypeSuite.scala
@@ -666,4 +666,63 @@ class TypeSuite extends BaseDottySuite {
     runTestAssert[Stat](code)(tree)
   }
 
+  // https://dotty.epfl.ch/docs/reference/experimental/cc.html
+
+  test("#3996 capture checking: 1") {
+    implicit val dialect: Dialect = dialects.Scala3Future
+    val code = "class Logger(fs: FileSystem^)"
+    val layout = "class Logger(fs: FileSystem^)"
+    val tree = Defn.Class(
+      Nil,
+      pname("Logger"),
+      Nil,
+      ctorp(tparam("fs", Type.Capturing("FileSystem", Nil))),
+      tplNoBody()
+    )
+    runTestAssert[Stat](code, layout)(tree)
+  }
+
+  test("#3996 capture checking: 2") {
+    implicit val dialect: Dialect = dialects.Scala3Future
+    val code = "val l: Logger^{fs} = Logger(fs)"
+    val layout = "val l: Logger^{fs} = Logger(fs)"
+    val tree = Defn.Val(
+      Nil,
+      List(Pat.Var("l")),
+      Some(Type.Capturing("Logger", List("fs"))),
+      Term.Apply("Logger", List("fs"))
+    )
+    runTestAssert[Stat](code, layout)(tree)
+  }
+
+  test("#3996 capture checking: 3") {
+    implicit val dialect: Dialect = dialects.Scala3Future
+    val code = "def tail: LazyList[A]^{this}"
+    val layout = "def tail: LazyList[A]^{this}"
+    val tree = Decl
+      .Def(Nil, "tail", Nil, Type.Capturing(Type.Apply("LazyList", List("A")), List(Term.This(anon))))
+    runTestAssert[Stat](code, layout)(tree)
+  }
+
+  test("#3996 capture checking: 3") {
+    implicit val dialect: Dialect = dialects.Scala3Future
+    val code = "def p: Pair[Int ->{ct} String, Logger^{fs}] = Pair(x, y)"
+    val layout = "def p: Pair[Int ->{ct} String, Logger^{fs}] = Pair(x, y)"
+    val tree = Defn.Def(
+      Nil,
+      "p",
+      Nil,
+      Some(Type.Apply(
+        pname("Pair"),
+        List(
+          Type.Capturing(purefunc(List("Int"), "String"), List("ct")),
+          Type.Capturing("Logger", List("fs"))
+        )
+      )),
+      Term.Apply("Pair", List("x", "y"))
+    )
+
+    runTestAssert[Stat](code, layout)(tree)
+  }
+
 }