Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,10 +320,11 @@ profiling {
**Origin:** cats.Monad
**Members:**
```scala
def flatMap[A, B](fa: F[A])(f: Function1[A, F[B]]): F[B]
def pure[A](x: A): F[A]
def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
def flatten[A](ffa: F[F[A]]): F[A]
def iterateWhile[A](f: F[A])(p: Function1[A, Boolean]): F[A]
def untilM[G[_], A](f: F[A])(cond: => F[Boolean])(implicit G: Alternative[G]): F[G[A]]
def compose[G[_]](implicit evidence$1: Applicative[G]): Applicative[[α] =>> F[G[α]]]
...
```

Expand All @@ -333,7 +334,7 @@ profiling {
<summary><code>cellar list-external org.typelevel:cats-core_3:2.10.0 cats --limit 5</code></summary>

object Eval$
trait ComposedContravariantCovariant[F, G] extends Contravariant[TypeLambda]
trait ComposedContravariantCovariant[F, G] extends Contravariant[[α] =>> F[G[α]]]
object Later$
object Show$
trait EvalSemigroup[A] extends Semigroup[Eval[A]]
Expand Down
6 changes: 6 additions & 0 deletions fixtureScala2/src/cellar/fixture/scala2/CellarCurried.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package cellar.fixture.scala2

/** Fixture for signature rendering: unbounded type params + curried parameter lists. */
trait CellarCurried {
def combine[A, B](a: A)(b: B): B
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package cellar.fixture.scala2

/** Fixture for signature rendering: higher-kinded type parameters. */
trait CellarHigherKinded {
def wrap[F[_], A](fa: F[A]): F[A]
def bounded[F[X <: AnyRef]](fa: F[String]): F[String]
def bimap[G[_, _], A, B](g: G[A, B]): G[A, B]
}
20 changes: 20 additions & 0 deletions fixtureScala2/src/cellar/fixture/scala2/CellarSugar.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package cellar.fixture.scala2

/** Fixture for signature rendering: function-type sugar + implicit lists. */
trait CellarShow[A] {
def show(a: A): A
}

trait CellarSugar {
def wildcard: List[_]
def boundedWildcard: List[_ <: AnyRef]

def transform[A, B](f: A => B): B
def zip[A, B, C](f: (A, B) => C): C
def nested[A, B, C](f: (A => B) => C): C
def thunk[A](f: () => A): A
def pair[A, B](t: (A, B)): (A, B)
def triple[A, B, C](t: (A, B, C)): (A, B, C)
def tupleArg[A, B](f: ((A, B)) => Boolean): Boolean
def withImplicit[A](a: A)(implicit s: CellarShow[A]): A
}
5 changes: 5 additions & 0 deletions fixtureScala3/src/cellar/fixture/scala3/CellarCurried.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package cellar.fixture.scala3

/** Fixture for signature rendering: unbounded type params + curried parameter lists. */
trait CellarCurried:
def combine[A, B](a: A)(b: B): B
15 changes: 15 additions & 0 deletions fixtureScala3/src/cellar/fixture/scala3/CellarHigherKinded.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package cellar.fixture.scala3

/** Fixture for signature rendering: higher-kinded type parameters. */
trait CellarBox[F[_]]

trait CellarBoundedBox[F[_ <: AnyRef]]

trait CellarHigherKinded:
def wrap[F[_], A](fa: F[A]): F[A]
def compose[F[_], G[_]](bf: CellarBox[F], bg: CellarBox[G]): CellarBox[[A] =>> F[G[A]]]
def composeBounded[F[_]](bf: CellarBox[F]): CellarBoundedBox[[A <: AnyRef] =>> F[A]]
def bounded[F[X <: AnyRef]](fa: F[String]): F[String]
def upper[F[A] <: Iterable[A]](fa: F[Int]): F[Int]
def selfBounded[F[A <: Comparable[A]]](fa: F[String]): F[String]
def bimap[G[_, _], A, B](g: G[A, B]): G[A, B]
25 changes: 25 additions & 0 deletions fixtureScala3/src/cellar/fixture/scala3/CellarSugar.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package cellar.fixture.scala3

/** Fixture for signature rendering: function-type sugar + implicit/using lists. */
trait CellarShow[A]:
def show(a: A): A

/** Symbolic binary type constructor, to exercise infix rendering (e.g. `F ~> G`). */
trait ~>[F[_], G[_]]:
def apply[A](fa: F[A]): G[A]

trait CellarSugar:
def mapK[F[_], G[_]](f: F ~> G): Unit
def wildcard: List[?]
def boundedWildcard: List[? <: AnyRef]

def transform[A, B](f: A => B): B
def zip[A, B, C](f: (A, B) => C): C
def nested[A, B, C](f: (A => B) => C): C
def thunk[A](f: () => A): A
def ctx[A, B](f: A ?=> B): B
def pair[A, B](t: (A, B)): (A, B)
def triple[A, B, C](t: (A, B, C)): (A, B, C)
def tupleArg[A, B](f: ((A, B)) => Boolean): Boolean
def withImplicit[A](a: A)(implicit s: CellarShow[A]): A
def withUsing[A](a: A)(using s: CellarShow[A]): A
171 changes: 143 additions & 28 deletions lib/src/cellar/TypePrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,28 @@ object TypePrinter:
t.prefix match
case NoPrefix => name
case _: ThisType => name
case p: Type if isPackageOrNone(p) => name
case p: Type if isElidedPrefix(p) => name
case p: Type => s"${printType(p)}.$name"
case _ => name

case t: AppliedType =>
val args = t.args.map(printTypeOrWildcard).mkString(", ")
s"${printType(t.tycon)}[$args]"
asFunction(t) match
case Some((contextual, params, result)) =>
val arrow = if contextual then " ?=> " else " => "
val lhs = params match
case single :: Nil if !functionArgNeedsParens(single) => printTypeOrWildcard(single)
case _ => params.map(printTypeOrWildcard).mkString("(", ", ", ")")
s"$lhs$arrow${printTypeOrWildcard(result)}"
case None =>
asTuple(t) match
case Some(elems) => elems.map(printTypeOrWildcard).mkString("(", ", ", ")")
case None =>
asInfix(t) match
case Some((lhs, op, rhs)) =>
s"${printTypeOrWildcard(lhs)} $op ${printTypeOrWildcard(rhs)}"
case None =>
val args = t.args.map(printTypeOrWildcard).mkString(", ")
s"${printType(t.tycon)}[$args]"

case t: ByNameType => s"=> ${printType(t.resultType)}"
case t: AndType => s"${printType(t.first)} & ${printType(t.second)}"
Expand All @@ -48,27 +63,29 @@ object TypePrinter:
case t: ConstantType => t.value.value.toString
case t: MatchType => s"${printType(t.scrutinee)} match { ... }"
case t: FlexibleType => printType(t.nonNullableType)
case t: TypeLambda =>
val params = t.paramNames.zip(t.paramTypeBounds).map(printTypeParam)
s"[${params.mkString(", ")}] =>> ${printType(t.resultType)}"
case _ => tpe.getClass.getSimpleName

def printMethodic(tpe: TypeOrMethodic)(using ctx: Context): String =
tpe match
case t: MethodType =>
val prefix = if t.isContextual then "using " else ""
val prefix =
if t.isContextual then "using "
else if t.isImplicit then "implicit "
else ""
val params = t.paramNames.zip(t.paramTypes).map { (n, tp) =>
s"$n: ${printType(tp)}"
}
val paramStr = s"($prefix${params.mkString(", ")})"
s"$paramStr: ${printMethodic(t.resultType)}"
val rest = t.resultType match
case _: MethodType | _: PolyType => printMethodic(t.resultType)
case r => s": ${printMethodic(r)}"
s"$paramStr$rest"

case t: PolyType =>
val typeParams = t.paramNames.zip(t.paramTypeBounds).map { (n, bounds) =>
bounds match
case b: AbstractTypeBounds =>
val lo = if b.low.toString == "Nothing" then "" else s" >: ${printType(b.low)}"
val hi = if b.high.toString == "Any" then "" else s" <: ${printType(b.high)}"
s"$n$lo$hi"
case _ => n.toString
}
val typeParams = t.paramNames.zip(t.paramTypeBounds).map(printTypeParam)
s"[${typeParams.mkString(", ")}]${printMethodic(t.resultType)}"

case t: Type => printType(t)
Expand All @@ -87,16 +104,14 @@ object TypePrinter:
case cls: ClassSymbol =>
val kind = if cls.isTrait then "trait" else if cls.isModuleClass then "object" else "class"
val typeParams = printClassTypeParams(cls.typeParams)
val parents = cls.parents.map(printType).filter(p => p != "Object" && p != "Any")
val parents = cls.parents.map(printParent).filter(p => p != "Object" && p != "Any")
val extendsStr = if parents.isEmpty then "" else s" extends ${parents.mkString(" with ")}"
s"$kind ${cls.name}$typeParams$extendsStr"

case term: TermSymbol =>
val keyword = termKeyword(term)
if term.isModuleVal then s"$keyword ${term.name}"
else
val sig = printMethodic(term.declaredType)
s"$keyword ${term.name}$sig"
else s"$keyword ${term.name}${printTopLevelMethodic(term.declaredType)}"

case other => other.toString

Expand All @@ -107,30 +122,130 @@ object TypePrinter:
else if sym.isModuleVal then "object"
else "val"

private def printClassTypeParams(params: List[ClassTypeParamSymbol]): String =
private def printTopLevelMethodic(tpe: TypeOrMethodic)(using ctx: Context): String =
tpe match
case t: Type => s": ${printType(t)}"
case t: PolyType =>
val typeParams = t.paramNames.zip(t.paramTypeBounds).map(printTypeParam)
s"[${typeParams.mkString(", ")}]${printTopLevelMethodic(t.resultType)}"
case t: MethodType => printMethodic(t)

private def printClassTypeParams(params: List[ClassTypeParamSymbol])(using ctx: Context): String =
if params.isEmpty then ""
else
val rendered = params.map { p =>
val variance = p.declaredVariance.productPrefix match
case "Covariant" => "+"
case "Contravariant" => "-"
case _ => ""
s"$variance${p.name}"
}
val rendered = params.map(printClassTypeParam)
s"[${rendered.mkString(", ")}]"

private def printClassTypeParam(param: ClassTypeParamSymbol)(using ctx: Context): String =
val variance = param.declaredVariance.productPrefix match
case "Covariant" => "+"
case "Contravariant" => "-"
case _ => ""
s"$variance${printTypeParam(param.name, param.declaredBounds)}"

private def printTypeParam(name: tastyquery.Names.TypeName, bounds: TypeBounds)(using ctx: Context): String =
bounds match
case b: AbstractTypeBounds =>
b.high match
case tl: TypeLambda => s"$name${printHkParams(tl)}"
case _ =>
val lo = if printType(b.low) == "Nothing" then "" else s" >: ${printType(b.low)}"
val hi = if printType(b.high) == "Any" then "" else s" <: ${printType(b.high)}"
s"$name$lo$hi"
case _ => name.toString

private def printHkParams(tl: TypeLambda)(using ctx: Context): String =
val rendered = tl.paramNames.zip(tl.paramTypeBounds).map { (name, bounds) =>
val (lo, hi) = bounds match
case b: AbstractTypeBounds =>
val lo = if printType(b.low) == "Nothing" then "" else s" >: ${printType(b.low)}"
val hi = if printType(b.high) == "Any" then "" else s" <: ${printType(b.high)}"
(lo, hi)
case _ => ("", "")
(name.toString, lo, hi)
}
// A parameter must be named (not `_`) when it is referenced elsewhere in the lambda:
// in a bound (self-referential, e.g. `A <: Comparable[A]`) or in the constructor
// bound (the lambda's result, e.g. `F[A] <: Iterable[A]`).
val resultStr = printType(tl.resultType)
val context = (resultStr :: rendered.flatMap((_, lo, hi) => List(lo, hi))).mkString(" ")
val params = rendered.map { (name, lo, hi) =>
val head = if isMentioned(context, name) then name else "_"
s"$head$lo$hi"
}
val constructorBound = if resultStr == "Any" then "" else s" <: $resultStr"
s"[${params.mkString(", ")}]$constructorBound"

private def isMentioned(context: String, name: String): Boolean =
context.matches(s"(?s).*\\b${java.util.regex.Pattern.quote(name)}\\b.*")

private def printTypeOrWildcard(tow: TypeOrWildcard)(using ctx: Context): String =
tow match
case w: WildcardTypeArg =>
w.bounds match
case b: AbstractTypeBounds =>
val lo = if b.low.toString == "Nothing" then "" else s" >: ${printType(b.low)}"
val hi = if b.high.toString == "Any" then "" else s" <: ${printType(b.high)}"
val lo = if printType(b.low) == "Nothing" then "" else s" >: ${printType(b.low)}"
val hi = if printType(b.high) == "Any" then "" else s" <: ${printType(b.high)}"
if lo.isEmpty && hi.isEmpty then "?" else s"?$lo$hi"
case _ => "?"
case t: Type => printType(t)

private def isPackageOrNone(prefix: Type): Boolean =
/** Render a class parent, parenthesising function-arrow sugar so it is valid in `extends` position. */
private def printParent(tpe: Type)(using ctx: Context): String =
val rendered = printType(tpe)
tpe match
case t: AppliedType if asFunction(t).isDefined => s"($rendered)"
case _ => rendered

private def isElidedPrefix(prefix: Type): Boolean =
prefix match
case _: ThisType => true
case t: TermRef => t.name.toString == "Predef" || t.name.toString == "package"
case _ => false

/** Decompose `scala.FunctionN` / `scala.ContextFunctionN` into (isContextual, params, result). */
private def asFunction(t: AppliedType): Option[(Boolean, List[TypeOrWildcard], TypeOrWildcard)] =
t.tycon match
case tycon: TypeRef if isScalaPackage(tycon.prefix) =>
val name = tycon.name.toString
val decoded =
if name.startsWith("ContextFunction") then Some((true, name.stripPrefix("ContextFunction")))
else if name.startsWith("Function") then Some((false, name.stripPrefix("Function")))
else None
decoded.flatMap { (contextual, digits) =>
digits.toIntOption
.filter(arity => arity >= 0 && t.args.sizeIs == arity + 1)
.map(_ => (contextual, t.args.init, t.args.last))
}
case _ => None

/** Decompose `scala.TupleN` (arity >= 2) into its element types. */
private def asTuple(t: AppliedType): Option[List[TypeOrWildcard]] =
t.tycon match
case tycon: TypeRef if isScalaPackage(tycon.prefix) =>
tycon.name.toString.stripPrefix("Tuple").toIntOption
.filter(arity => arity >= 2 && t.args.sizeIs == arity)
.map(_ => t.args)
case _ => None

/** Decompose a binary applied type whose tycon is a symbolic operator (e.g. `F ~> G`). */
private def asInfix(t: AppliedType): Option[(TypeOrWildcard, String, TypeOrWildcard)] =
t.tycon match
case tycon: TypeRef if t.args.sizeIs == 2 && isOperatorName(tycon.name.toString) =>
Some((t.args.head, tycon.name.toString, t.args(1)))
case _ => None

/** An identifier composed solely of operator characters (no letters/digits/`_`/`$`). */
private def isOperatorName(name: String): Boolean =
name.nonEmpty && name.forall(c => !c.isLetterOrDigit && c != '_' && c != '$')

private def isScalaPackage(prefix: Prefix): Boolean =
prefix match
case p: PackageRef => p.fullyQualifiedName.toString == "scala"
case _ => false

/** A function- or tuple-typed left operand of `=>` must be parenthesised: `(A => B) => C`, `((A, B)) => C`. */
private def functionArgNeedsParens(tow: TypeOrWildcard): Boolean =
tow match
case t: AppliedType => asFunction(t).isDefined || asTuple(t).isDefined
case _ => false
Loading