Skip to content

Implement value classes #411

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
May 1, 2015
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
4 changes: 3 additions & 1 deletion src/dotty/tools/dotc/Compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ class Compiler {
new ElimByName,
new ResolveSuper),
List(new Erasure),
List(new Mixin,
List(new ElimErasedValueType,
new VCInline,
new Mixin,
new LazyVals,
new Memoize,
new CapturedVars, // capturedVars has a transformUnit: no phases should introduce local mutable vars here
Expand Down
2 changes: 2 additions & 0 deletions src/dotty/tools/dotc/core/Phases.scala
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ object Phases {

private val typerCache = new PhaseCache(classOf[FrontEnd])
private val refChecksCache = new PhaseCache(classOf[RefChecks])
private val extensionMethodsCache = new PhaseCache(classOf[ExtensionMethods])
private val erasureCache = new PhaseCache(classOf[Erasure])
private val patmatCache = new PhaseCache(classOf[PatternMatcher])
private val flattenCache = new PhaseCache(classOf[Flatten])
Expand All @@ -241,6 +242,7 @@ object Phases {

def typerPhase = typerCache.phase
def refchecksPhase = refChecksCache.phase
def extensionMethodsPhase = extensionMethodsCache.phase
def erasurePhase = erasureCache.phase
def patmatPhase = patmatCache.phase
def flattenPhase = flattenCache.phase
Expand Down
2 changes: 2 additions & 0 deletions src/dotty/tools/dotc/core/StdNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ object StdNames {
val ANYname: N = "<anyname>"
val CONSTRUCTOR: N = Names.CONSTRUCTOR.toString
val DEFAULT_CASE: N = "defaultCase$"
val EVT2U: N = "evt2u$"
val EQEQ_LOCAL_VAR: N = "eqEqTemp$"
val FAKE_LOCAL_THIS: N = "this$"
val IMPLCLASS_CONSTRUCTOR: N = "$init$"
Expand Down Expand Up @@ -257,6 +258,7 @@ object StdNames {
val SKOLEM: N = "<skolem>"
val SPECIALIZED_INSTANCE: N = "specInstance$"
val THIS: N = "_$this"
val U2EVT: N = "u2evt$"

final val Nil: N = "Nil"
final val Predef: N = "Predef"
Expand Down
10 changes: 8 additions & 2 deletions src/dotty/tools/dotc/core/SymDenotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1447,10 +1447,16 @@ object SymDenotations {

def inCache(tp: Type) = baseTypeRefCache.containsKey(tp)

/** Can't cache types containing type variables which are uninstantiated
* or whose instances can change, depending on typerstate.
/** We cannot cache:
* - type variables which are uninstantiated or whose instances can
* change, depending on typerstate.
* - types where the underlying type is an ErasedValueType, because
* this underlying type will change after ElimErasedValueType,
* and this changes subtyping relations. As a shortcut, we do not
* cache ErasedValueType at all.
*/
def isCachable(tp: Type): Boolean = tp match {
case _: TypeErasure.ErasedValueType => false
case tp: TypeVar => tp.inst.exists && inCache(tp.inst)
case tp: TypeProxy => inCache(tp.underlying)
case tp: AndOrType => inCache(tp.tp1) && inCache(tp.tp2)
Expand Down
8 changes: 0 additions & 8 deletions src/dotty/tools/dotc/core/Symbols.scala
Original file line number Diff line number Diff line change
Expand Up @@ -434,14 +434,6 @@ object Symbols {
/** If this symbol satisfies predicate `p` this symbol, otherwise `NoSymbol` */
def filter(p: Symbol => Boolean): Symbol = if (p(this)) this else NoSymbol

/** Is this symbol a user-defined value class? */
final def isDerivedValueClass(implicit ctx: Context): Boolean = {
this.derivesFrom(defn.AnyValClass)(ctx.withPhase(denot.validFor.firstPhaseId))
// Simulate ValueClasses.isDerivedValueClass
false // will migrate to ValueClasses.isDerivedValueClass;
// unsupported value class code will continue to use this stub while it exists
}

/** The current name of this symbol */
final def name(implicit ctx: Context): ThisName = denot.name.asInstanceOf[ThisName]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sym.isDerivedValueClass feels more preferable to me than isDerivedValueClass(sym)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I wasn't sure about that, it seems nice to keep all the value class methods in ValueClasses.scala, but I can move that method in Symbols if you think that's better.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can remain in ValueClasses but could become a method on implicit decorator.


Expand Down
8 changes: 8 additions & 0 deletions src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ class TypeComparer(initctx: Context) extends DotClass with ConstraintHandling wi
compareSuper
case AndType(tp21, tp22) =>
isSubType(tp1, tp21) && isSubType(tp1, tp22)
case TypeErasure.ErasedValueType(cls2, underlying2) =>
def compareErasedValueType = tp1 match {
case TypeErasure.ErasedValueType(cls1, underlying1) =>
(cls1 eq cls2) && isSameType(underlying1, underlying2)
case _ =>
secondTry(tp1, tp2)
}
compareErasedValueType
case ErrorType =>
true
case _ =>
Expand Down
143 changes: 89 additions & 54 deletions src/dotty/tools/dotc/core/TypeErasure.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ package dotc
package core

import Symbols._, Types._, Contexts._, Flags._, Names._, StdNames._, Decorators._, Flags.JavaDefined
import Uniques.unique
import dotc.transform.ExplicitOuter._
import dotc.transform.ValueClasses._
import typer.Mode
import util.DotClass

/** Erased types are:
*
* ErasedValueType
* TypeRef(prefix is ignored, denot is ClassDenotation)
* TermRef(prefix is ignored, denot is SymDenotation)
* JavaArrayType
Expand All @@ -29,8 +32,12 @@ object TypeErasure {

/** A predicate that tests whether a type is a legal erased type. Only asInstanceOf and
* isInstanceOf may have types that do not satisfy the predicate.
* ErasedValueType is considered an erased type because it is valid after Erasure (it is
* eliminated by ElimErasedValueType).
*/
def isErasedType(tp: Type)(implicit ctx: Context): Boolean = tp match {
case _: ErasedValueType =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to update comment before class to include ErasedValueType

true
case tp: TypeRef =>
tp.symbol.isClass && tp.symbol != defn.AnyClass
case _: TermRef =>
Expand All @@ -51,55 +58,68 @@ object TypeErasure {
false
}

case class ErasedValueType(cls: ClassSymbol, underlying: Type) extends CachedGroundType {
override def computeHash = doHash(cls, underlying)
/** A type representing the semi-erasure of a derived value class, see SIP-15
* where it's called "C$unboxed" for a class C.
* Derived value classes are erased to this type during Erasure (when
* semiEraseVCs = true) and subsequently erased to their underlying type
* during ElimErasedValueType. This type is outside the normal Scala class
* hierarchy: it is a subtype of no other type and is a supertype only of
* Nothing. This is because this type is only useful for type adaptation (see
* [[Erasure.Boxing#adaptToType]]).
*
* @param cls The value class symbol
* @param erasedUnderlying The erased type of the single field of the value class
*/
abstract case class ErasedValueType(cls: ClassSymbol, erasedUnderlying: Type)
extends CachedGroundType with ValueType {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say it needs documentation. And I'd like it to include why it's a GroundType, not a ProxyType.

override def computeHash = doHash(cls, erasedUnderlying)
}

private def erasureIdx(isJava: Boolean, isSemi: Boolean, isConstructor: Boolean, wildcardOK: Boolean) =
final class CachedErasedValueType(cls: ClassSymbol, erasedUnderlying: Type)
extends ErasedValueType(cls, erasedUnderlying)

object ErasedValueType {
def apply(cls: ClassSymbol, erasedUnderlying: Type)(implicit ctx: Context) = {
unique(new CachedErasedValueType(cls, erasedUnderlying))
}
}

private def erasureIdx(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean, wildcardOK: Boolean) =
(if (isJava) 1 else 0) +
(if (isSemi) 2 else 0) +
(if (semiEraseVCs) 2 else 0) +
(if (isConstructor) 4 else 0) +
(if (wildcardOK) 8 else 0)

private val erasures = new Array[TypeErasure](16)

for {
isJava <- List(false, true)
isSemi <- List(false, true)
semiEraseVCs <- List(false, true)
isConstructor <- List(false, true)
wildcardOK <- List(false, true)
} erasures(erasureIdx(isJava, isSemi, isConstructor, wildcardOK)) =
new TypeErasure(isJava, isSemi, isConstructor, wildcardOK)
} erasures(erasureIdx(isJava, semiEraseVCs, isConstructor, wildcardOK)) =
new TypeErasure(isJava, semiEraseVCs, isConstructor, wildcardOK)

/** Produces an erasure function.
* @param isJava Arguments should be treated the way Java does it
* @param isSemi Value classes are mapped in an intermediate step to
* ErasedValueClass types, instead of going directly to
* the erasure of the underlying type.
* @param isConstructor Argument forms part of the type of a constructor
* @param wildcardOK Wildcards are acceptable (true when using the erasure
* for computing a signature name).
/** Produces an erasure function. See the documentation of the class [[TypeErasure]]
* for a description of each parameter.
*/
private def erasureFn(isJava: Boolean, isSemi: Boolean, isConstructor: Boolean, wildcardOK: Boolean): TypeErasure =
erasures(erasureIdx(isJava, isSemi, isConstructor, wildcardOK))

private val scalaErasureFn = erasureFn(isJava = false, isSemi = false, isConstructor = false, wildcardOK = false)
private val scalaSigFn = erasureFn(isJava = false, isSemi = false, isConstructor = false, wildcardOK = true)
private val javaSigFn = erasureFn(isJava = true, isSemi = false, isConstructor = false, wildcardOK = true)
private val semiErasureFn = erasureFn(isJava = false, isSemi = true, isConstructor = false, wildcardOK = false)
private def erasureFn(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean, wildcardOK: Boolean): TypeErasure =
erasures(erasureIdx(isJava, semiEraseVCs, isConstructor, wildcardOK))

/** The current context with a phase no later than erasure */
private def erasureCtx(implicit ctx: Context) =
if (ctx.erasedTypes) ctx.withPhase(ctx.erasurePhase).addMode(Mode.FutureDefsOK) else ctx

def erasure(tp: Type)(implicit ctx: Context): Type = scalaErasureFn(tp)(erasureCtx)
def semiErasure(tp: Type)(implicit ctx: Context): Type = semiErasureFn(tp)(erasureCtx)
def erasure(tp: Type, semiEraseVCs: Boolean = true)(implicit ctx: Context): Type =
erasureFn(isJava = false, semiEraseVCs, isConstructor = false, wildcardOK = false)(tp)(erasureCtx)

def sigName(tp: Type, isJava: Boolean)(implicit ctx: Context): TypeName = {
val seqClass = if (isJava) defn.ArrayClass else defn.SeqClass
val normTp =
if (tp.isRepeatedParam) tp.translateParameterized(defn.RepeatedParamClass, seqClass)
else tp
(if (isJava) javaSigFn else scalaSigFn).sigName(normTp)(erasureCtx)
val erase = erasureFn(isJava, semiEraseVCs = false, isConstructor = false, wildcardOK = true)
erase.sigName(normTp)(erasureCtx)
}

/** The erasure of a top-level reference. Differs from normal erasure in that
Expand All @@ -117,29 +137,20 @@ object TypeErasure {
erasure(tp)
}

/** The erasure of a symbol's info. This is different of `erasure` in the way `ExprType`s are
* treated. `eraseInfo` maps them them to nullary method types, whereas `erasure` maps them
* to `Function0`.
*/
def eraseInfo(tp: Type, sym: Symbol)(implicit ctx: Context): Type =
scalaErasureFn.eraseInfo(tp, sym)(erasureCtx)

/** The erasure of a function result type. Differs from normal erasure in that
* Unit is kept instead of being mapped to BoxedUnit.
*/
def eraseResult(tp: Type)(implicit ctx: Context): Type =
scalaErasureFn.eraseResult(tp)(erasureCtx)

/** The symbol's erased info. This is the type's erasure, except for the following symbols:
*
* - For $asInstanceOf : [T]T
* - For $isInstanceOf : [T]Boolean
* - For all abstract types : = ?
* - For COMPANION_CLASS_METHOD : the erasure of their type with semiEraseVCs = false,
* this is needed to keep [[SymDenotation#companionClass]]
* working after erasure for value classes.
* - For all other symbols : the semi-erasure of their types, with
* isJava, isConstructor set according to symbol.
*/
def transformInfo(sym: Symbol, tp: Type)(implicit ctx: Context): Type = {
val erase = erasureFn(sym is JavaDefined, isSemi = true, sym.isConstructor, wildcardOK = false)
val semiEraseVCs = sym.name ne nme.COMPANION_CLASS_METHOD
val erase = erasureFn(sym is JavaDefined, semiEraseVCs, sym.isConstructor, wildcardOK = false)

def eraseParamBounds(tp: PolyType): Type =
tp.derivedPolyType(
Expand All @@ -148,7 +159,7 @@ object TypeErasure {
if (defn.isPolymorphicAfterErasure(sym)) eraseParamBounds(sym.info.asInstanceOf[PolyType])
else if (sym.isAbstractType) TypeAlias(WildcardType)
else if (sym.isConstructor) outer.addParam(sym.owner.asClass, erase(tp)(erasureCtx))
else eraseInfo(tp, sym)(erasureCtx) match {
else erase.eraseInfo(tp, sym)(erasureCtx) match {
case einfo: MethodType if sym.isGetter && einfo.resultType.isRef(defn.UnitClass) =>
defn.BoxedUnitClass.typeRef
case einfo =>
Expand Down Expand Up @@ -241,12 +252,15 @@ object TypeErasure {
import TypeErasure._

/**
* This is used as the Scala erasure during the erasure phase itself
* It differs from normal erasure in that value classes are erased to ErasedValueTypes which
* are then later converted to the underlying parameter type in phase posterasure.
*
* @param isJava Arguments should be treated the way Java does it
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to include more details on the actual difference

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was just moved from the documentation of erasureFn, I don't know enough about java erasure to improve this comment myself :).

* @param semiEraseVCs If true, value classes are semi-erased to ErasedValueType
* (they will be fully erased in [[ElimErasedValueType]]).
* If false, they are erased like normal classes.
* @param isConstructor Argument forms part of the type of a constructor
* @param wildcardOK Wildcards are acceptable (true when using the erasure
* for computing a signature name).
*/
class TypeErasure(isJava: Boolean, isSemi: Boolean, isConstructor: Boolean, wildcardOK: Boolean) extends DotClass {
class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean, wildcardOK: Boolean) extends DotClass {

/** The erasure |T| of a type T. This is:
*
Expand Down Expand Up @@ -279,10 +293,12 @@ class TypeErasure(isJava: Boolean, isSemi: Boolean, isConstructor: Boolean, wild
* - For any other type, exception.
*/
private def apply(tp: Type)(implicit ctx: Context): Type = tp match {
case _: ErasedValueType =>
tp
case tp: TypeRef =>
val sym = tp.symbol
if (!sym.isClass) this(tp.info)
else if (sym.isDerivedValueClass) eraseDerivedValueClassRef(tp)
else if (semiEraseVCs && isDerivedValueClass(sym)) eraseDerivedValueClassRef(tp)
else eraseNormalClassRef(tp)
case tp: RefinedType =>
val parent = tp.parent
Expand All @@ -291,7 +307,9 @@ class TypeErasure(isJava: Boolean, isSemi: Boolean, isConstructor: Boolean, wild
case tp: TermRef =>
this(tp.widen)
case tp: ThisType =>
this(tp.cls.typeRef)
def thisTypeErasure(tpToErase: Type) =
erasureFn(isJava, semiEraseVCs = false, isConstructor, wildcardOK)(tpToErase)
thisTypeErasure(tp.cls.typeRef)
case SuperType(thistpe, supertpe) =>
SuperType(this(thistpe), this(supertpe))
case ExprType(rt) =>
Expand All @@ -303,7 +321,8 @@ class TypeErasure(isJava: Boolean, isSemi: Boolean, isConstructor: Boolean, wild
case OrType(tp1, tp2) =>
ctx.typeComparer.orType(this(tp1), this(tp2), erased = true)
case tp: MethodType =>
val paramErasure = erasureFn(tp.isJava, isSemi, isConstructor, wildcardOK)(_)
def paramErasure(tpToErase: Type) =
erasureFn(tp.isJava, semiEraseVCs, isConstructor, wildcardOK)(tpToErase)
val formals = tp.paramTypes.mapConserve(paramErasure)
eraseResult(tp.resultType) match {
case rt: MethodType =>
Expand Down Expand Up @@ -341,11 +360,17 @@ class TypeErasure(isJava: Boolean, isSemi: Boolean, isConstructor: Boolean, wild

private def eraseArray(tp: RefinedType)(implicit ctx: Context) = {
val defn.ArrayType(elemtp) = tp
def arrayErasure(tpToErase: Type) =
erasureFn(isJava, semiEraseVCs = false, isConstructor, wildcardOK)(tpToErase)
if (elemtp derivesFrom defn.NullClass) JavaArrayType(defn.ObjectType)
else if (isUnboundedGeneric(elemtp)) defn.ObjectType
else JavaArrayType(this(elemtp))
else JavaArrayType(arrayErasure(elemtp))
}

/** The erasure of a symbol's info. This is different from `apply` in the way `ExprType`s are
* treated. `eraseInfo` maps them them to nullary method types, whereas `apply` maps them
* to `Function0`.
*/
def eraseInfo(tp: Type, sym: Symbol)(implicit ctx: Context) = tp match {
case ExprType(rt) =>
if (sym is Param) apply(tp)
Expand All @@ -354,22 +379,30 @@ class TypeErasure(isJava: Boolean, isSemi: Boolean, isConstructor: Boolean, wild
// forwarders to mixin methods.
// See doc comment for ElimByName for speculation how we could improve this.
else MethodType(Nil, Nil, eraseResult(rt))
case tp => erasure(tp)
case tp => this(tp)
}

private def eraseDerivedValueClassRef(tref: TypeRef)(implicit ctx: Context): Type = {
val cls = tref.symbol.asClass
val underlying = underlyingOfValueClass(cls)
ErasedValueType(cls, erasure(underlying))
}

private def eraseDerivedValueClassRef(tref: TypeRef)(implicit ctx: Context): Type =
unsupported("eraseDerivedValueClass")

private def eraseNormalClassRef(tref: TypeRef)(implicit ctx: Context): Type = {
val cls = tref.symbol.asClass
(if (cls.owner is Package) normalizeClass(cls) else cls).typeRef
}

/** The erasure of a function result type. */
private def eraseResult(tp: Type)(implicit ctx: Context): Type = tp match {
case tp: TypeRef =>
val sym = tp.typeSymbol
if (sym eq defn.UnitClass) sym.typeRef
else if (sym.isDerivedValueClass) eraseNormalClassRef(tp)
// For a value class V, "new V(x)" should have type V for type adaptation to work
// correctly (see SIP-15 and [[Erasure.Boxing.adaptToType]]), so the return type of a
// constructor method should not be semi-erased.
else if (isConstructor && isDerivedValueClass(sym)) eraseNormalClassRef(tp)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are you testing for constructor here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type of the constructor of a value class should not be erased: new Meter(4) should have type Meter. The return type of any other method can be safely erased, Do you think this is worth a comment?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I would say it is worth a comment, that also tells should it be Meter or EVT(Meter)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After thinking more about it, I do not see why new Meter(4) is special here. There are numerous ways to instantiate a class(eg use clone on existing one), and from type system point of view new is not different from them. You also say later that you need to leave un-erased TypeTrees for other poly-methods, not only New.
Can you elaborate on this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Value classes are unboxed when there is a type mismatch that can be solved by adaptToType: if new Meter(5) has type Meter but the expected type is ErasedValueType(Meter, Int) then adaptToType will unbox new Meter(5). This is the correct behavior and will not happen if the type of new Meter(5) is ErasedValueType(Meter, Int) (which would be wrong because the constructor returns an object of type Meter). clone() is not a member of AnyVal so I don't need to handle it separately, but let me know if you can think of some other case I'd need to handle.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, resolved. But please include this comment in source code, or a short comment here, and a full one in markdown doc that you were proposing.

else this(tp)
case RefinedType(parent, _) if !(parent isRef defn.ArrayClass) =>
eraseResult(parent)
Expand All @@ -391,10 +424,12 @@ class TypeErasure(isJava: Boolean, isSemi: Boolean, isConstructor: Boolean, wild
* Need to ensure correspondence with erasure!
*/
private def sigName(tp: Type)(implicit ctx: Context): TypeName = tp match {
case ErasedValueType(_, underlying) =>
sigName(underlying)
case tp: TypeRef =>
val sym = tp.symbol
if (!sym.isClass) sigName(tp.info)
else if (sym.isDerivedValueClass) sigName(eraseDerivedValueClassRef(tp))
else if (isDerivedValueClass(sym)) sigName(eraseDerivedValueClassRef(tp))
else normalizeClass(sym.asClass).fullName.asTypeName
case defn.ArrayType(elem) =>
sigName(this(tp))
Expand Down
3 changes: 3 additions & 0 deletions src/dotty/tools/dotc/printing/RefinedPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package printing

import core._
import Texts._, Types._, Flags._, Names._, Symbols._, NameOps._, Constants._
import TypeErasure.ErasedValueType
import Contexts.Context, Scopes.Scope, Denotations._, SymDenotations._, Annotations.Annotation
import StdNames.nme
import ast.{Trees, untpd, tpd}
Expand Down Expand Up @@ -132,6 +133,8 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
return toText(tp.info)
case ExprType(result) =>
return "=> " ~ toText(result)
case ErasedValueType(clazz, underlying) =>
return "ErasedValueType(" ~ toText(clazz.typeRef) ~ ", " ~ toText(underlying) ~ ")"
case tp: ClassInfo =>
return toTextParents(tp.instantiatedParents) ~ "{...}"
case JavaArrayType(elemtp) =>
Expand Down
Loading