Difference between Wildcard types and TypeBounds?

I was wondering what’s the difference between Wildcard types and TypeBounds in the compiler.

The language specification seems to only allow Wildcards as arguments to ParameterizedType’s (I’m assuming these correspond to AppliedType’s in the implementation), and always require them to have TypeBounds. Whereas TypeBounds can be used to add more information to TypeParam’s or Refinement’s (and Wildcard’s), but not as a standalone type. Does this match how it is/should be used in the compiler?

The Tasty format mentions TypeBounds in a few places, but not Wildcard types. If we were to attempt to pickle a type with a Wildcard type it would throw a MatchError in TreePickler (I’m not sure if this can be triggered organically in the current compiler). Is the fact that Tasty doesn’t support Wildcard types intentional?

TypeBounds can be created by the user (e.g. Foo[? <: Bar]). Wildcard types can be created by the SpaceEngine when it indirectly calls ProtoTypes.wildApprox. Are there other ways the user or the compiler creates Wildcard types? When should one be used over the other?

The compiler’s TypeBounds acts both as the spec’s abstract type definition (TypeBounds) and wildcard type argument (WildcardTypeArg), depending on where it is used. TASTy has the same double usage.

What the compiler calls a WildcardType does not exist in the spec nor in TASTy. It is an internal thing used for type inference and elaboration only.

Thank you for the explanation!
I have some follow-up questions:

Is WildcardType related to the “?” type in Colored Local Type Inference paper?

If WildcardTypes are only used for inference and elaboration, is the assumption that they won’t be used/present after the Typer phase?

Yes indeed it’s the same idea.

They shouldn’t be present in the type of any tree at any phase, just in “prototypes” used temporarily in the compiler. This might happen at different phases, for example they’re used during implicit resolution which can happen after Typer (in particular when using summonInline in a non-transparent inline def).

I see.
This question was originally prompted because I’m trying to better understand TypeOps.refineUsingParent, which will add WildcardTypes instead of TypeBounds to the type parameters it returns.

With the following source code:

sealed trait Foo[+A]
case class Bar[A](a: A) extends Foo[A]
case object Baz extends Foo[Nothing]

def unFoo[A](f: Foo[A]): Option[A] = f match {
  case Bar(a) => Some(a)
  case _      => None

Trace of running refineUsingParent( Foo[A], Bar ) returns a WildcardType (shown as <? <: A>):

==> refineUsingParent(parent = Foo[A], child = class Bar, mixins = [])?
  ==> instantiateToSubType(Bar, Foo[A], [])?
    ==> instantiate()?
      ==> wildApprox(tp = Bar[A(param)18$1])?
        ==> wildApprox(tp = Bar)?
        <== wildApprox(tp = Bar) = Bar
        ==> wildApprox(tp = A(param)18$1)?
          ==> wildApprox(tp = A(param)18$1)?
          <== wildApprox(tp = A(param)18$1) = <? <: A>
        <== wildApprox(tp = A(param)18$1) = <? <: A>
      <== wildApprox(tp = Bar[A(param)18$1]) = Bar[<? <: A>]
    <== instantiate() = Bar[<? <: A>]
  <== instantiateToSubType(Bar, Foo[A], []) = Bar[<? <: A>]
<== refineUsingParent(parent = Foo[A], child = class Bar, mixins = []) = Bar[<? <: A>]

As far as I can tell refineUsingParent is only used in the SpaceEngine right now, and the resulting type is never stored in a tree.

However, the question I have is whether it should return types containing WildcardTypes instead of TypeBounds, given that it’s not really used as a “prototype”?

When WildcardType is used in a prototype, what is the difference between a WildcardType that just wraps a TypeBounds and the underlying TypeBounds itself? Another way of phrasing the question: in a prototype, why does the compiler use a WildcardType instead of just the TypeBounds?

I think the simple answer can be: TypeBounds is a TypeType, not a TermType, so it is only used to represent the information of a type.

I think you’re right, it seems like TypeBounds should be used here and this only works by chance currently.

Yeah I think that’s a good way to put it.