Skip to content

Commit f1156d9

Browse files
committed
Implement simple phantom functions
Simple phantom functions are FunctionN/ImplicitFunctionN types where all parameters are of the same phantom universe. They are synthesized in the object defining the phantom universe. It main usage should be through `(MyPhantom) => Int` nothation as it does not require any imports.
1 parent 7bf86d2 commit f1156d9

40 files changed

+579
-51
lines changed

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 78 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,14 @@ class Definitions {
6060
private def enterCompleteClassSymbol(owner: Symbol, name: TypeName, flags: FlagSet, parents: List[TypeRef], decls: Scope = newScope) =
6161
ctx.newCompleteClassSymbol(owner, name, flags | Permanent, parents, decls).entered
6262

63-
private def enterTypeField(cls: ClassSymbol, name: TypeName, flags: FlagSet, scope: MutableScope) =
64-
scope.enter(newSymbol(cls, name, flags, TypeBounds.empty))
63+
private def enterTypeField(cls: ClassSymbol, name: TypeName, flags: FlagSet, scope: MutableScope, typeBounds: TypeBounds) =
64+
scope.enter(newSymbol(cls, name, flags, typeBounds))
6565

66-
private def enterTypeParam(cls: ClassSymbol, name: TypeName, flags: FlagSet, scope: MutableScope) =
67-
enterTypeField(cls, name, flags | ClassTypeParamCreationFlags, scope)
66+
private def enterTypeParam(cls: ClassSymbol, name: TypeName, flags: FlagSet, scope: MutableScope, typeBounds: TypeBounds) =
67+
enterTypeField(cls, name, flags | ClassTypeParamCreationFlags, scope, typeBounds)
6868

6969
private def enterSyntheticTypeParam(cls: ClassSymbol, paramFlags: FlagSet, scope: MutableScope, suffix: String = "T0") =
70-
enterTypeParam(cls, suffix.toTypeName.expandedName(cls), paramFlags, scope)
70+
enterTypeParam(cls, suffix.toTypeName.expandedName(cls), paramFlags, scope, TypeBounds.empty)
7171

7272
// NOTE: Ideally we would write `parentConstrs: => Type*` but SIP-24 is only
7373
// implemented in Dotty and not in Scala 2.
@@ -104,33 +104,31 @@ class Definitions {
104104
* def apply(implicit $x0: T0, ..., $x{N_1}: T{N-1}): R
105105
* }
106106
*/
107-
def newFunctionNTrait(name: TypeName): ClassSymbol = {
107+
def newFunctionNTrait(name: TypeName, lattice: Symbol): ClassSymbol = {
108108
val completer = new LazyType {
109109
def complete(denot: SymDenotation)(implicit ctx: Context): Unit = {
110110
val cls = denot.asClass.classSymbol
111111
val decls = newScope
112112
val arity = name.functionArity
113+
val top = lattice.thisType.select(tpnme.Any)
114+
val bottom = lattice.thisType.select(tpnme.Nothing)
113115
val paramNamePrefix = tpnme.scala_ ++ str.NAME_JOIN ++ name ++ str.EXPAND_SEPARATOR
114116
val argParams =
115117
for (i <- List.range(1, arity + 1)) yield
116-
enterTypeParam(cls, paramNamePrefix ++ "T" ++ i.toString, Contravariant, decls)
117-
val resParam = enterTypeParam(cls, paramNamePrefix ++ "R", Covariant, decls)
118+
enterTypeParam(cls, paramNamePrefix ++ "T" ++ i.toString, Contravariant, decls, TypeBounds(bottom, top)).typeRef
119+
val resParam = enterTypeParam(cls, paramNamePrefix ++ "R", Covariant, decls, TypeBounds.empty).typeRef
118120
val (methodType, parentTraits) =
119121
if (name.firstPart.startsWith(str.ImplicitFunction)) {
120122
val superTrait =
121-
FunctionType(arity).appliedTo(argParams.map(_.typeRef) ::: resParam.typeRef :: Nil)
123+
FunctionType(arity, isImplicit = false, top).appliedTo(argParams ::: resParam :: Nil)
122124
(ImplicitMethodType, superTrait :: Nil)
123125
}
124126
else (MethodType, Nil)
125-
val applyMeth =
126-
decls.enter(
127-
newMethod(cls, nme.apply,
128-
methodType(argParams.map(_.typeRef), resParam.typeRef), Deferred))
129-
denot.info =
130-
ClassInfo(ScalaPackageClass.thisType, cls, ObjectType :: parentTraits, decls)
127+
decls.enter(newMethod(cls, nme.apply, methodType(argParams, resParam), Deferred))
128+
denot.info = ClassInfo(lattice.thisType, cls, ObjectType :: parentTraits, decls)
131129
}
132130
}
133-
newClassSymbol(ScalaPackageClass, name, Trait | NoInits, completer)
131+
newClassSymbol(lattice, name, Trait | NoInits, completer)
134132
}
135133

136134
private def newMethod(cls: ClassSymbol, name: TermName, info: Type, flags: FlagSet = EmptyFlags): TermSymbol =
@@ -190,7 +188,7 @@ class Definitions {
190188
val cls = ScalaPackageVal.moduleClass.asClass
191189
cls.info.decls.openForMutations.useSynthesizer(
192190
name => ctx =>
193-
if (name.isTypeName && name.isSyntheticFunction) newFunctionNTrait(name.asTypeName)
191+
if (name.isTypeName && name.isSyntheticScalaFunction) newFunctionNTrait(name.asTypeName, cls)
194192
else NoSymbol)
195193
cls
196194
}
@@ -687,7 +685,7 @@ class Definitions {
687685

688686
object FunctionOf {
689687
def apply(args: List[Type], resultType: Type, isImplicit: Boolean = false)(implicit ctx: Context) =
690-
FunctionType(args.length, isImplicit).appliedTo(args ::: resultType :: Nil)
688+
FunctionType(args, resultType, isImplicit).appliedTo(args ::: resultType :: Nil)
691689
def unapply(ft: Type)(implicit ctx: Context) = {
692690
val tsym = ft.typeSymbol
693691
if (isFunctionClass(tsym)) {
@@ -767,16 +765,39 @@ class Definitions {
767765
lazy val Function0_applyR = ImplementedFunctionType(0).symbol.requiredMethodRef(nme.apply)
768766
def Function0_apply(implicit ctx: Context) = Function0_applyR.symbol
769767

770-
def FunctionType(n: Int, isImplicit: Boolean = false)(implicit ctx: Context): TypeRef =
771-
if (n <= MaxImplementedFunctionArity && (!isImplicit || ctx.erasedTypes)) ImplementedFunctionType(n)
768+
def FunctionType(n: Int, isImplicit: Boolean = false, top: Type = AnyType)(implicit ctx: Context): TypeRef = {
769+
if (top.isPhantom) {
770+
val functionPrefix = if (isImplicit) str.ImplicitFunction else str.Function
771+
val functionName = (functionPrefix + n).toTypeName
772+
val functionType = top.normalizedPrefix.select(functionName)
773+
assert(functionType.classSymbol.exists)
774+
functionType.asInstanceOf[TypeRef]
775+
}
776+
else if (n <= MaxImplementedFunctionArity && (!isImplicit || ctx.erasedTypes)) ImplementedFunctionType(n)
772777
else FunctionClass(n, isImplicit).typeRef
778+
}
779+
780+
def FunctionType(args: List[Type], resultType: Type, isImplicit: Boolean)(implicit ctx: Context): TypeRef =
781+
FunctionType(args.length, isImplicit, topInSameUniverse(args, "function arguments."))
782+
783+
private def topInSameUniverse(types: List[Type], relationship: => String)(implicit ctx: Context): Type = {
784+
types match {
785+
case first :: Nil => first.topType
786+
case first :: rest => (first /: rest)(inSameUniverse((t1, _) => t1.topType, _, _, relationship, ctx.owner.pos))
787+
case Nil => defn.AnyType
788+
}
789+
}
773790

774791
private lazy val TupleTypes: Set[TypeRef] = TupleType.toSet
775792

776793
/** If `cls` is a class in the scala package, its name, otherwise EmptyTypeName */
777794
def scalaClassName(cls: Symbol)(implicit ctx: Context): TypeName =
778795
if (cls.isClass && cls.owner == ScalaPackageClass) cls.asClass.name else EmptyTypeName
779796

797+
/** If `cls` is a class in an object extending scala.Phantom, its name, otherwise EmptyTypeName */
798+
def phantomClassName(cls: Symbol)(implicit ctx: Context): TypeName =
799+
if (cls.isClass && cls.owner.derivesFrom(PhantomClass)) cls.asClass.name else EmptyTypeName
800+
780801
/** If type `ref` refers to a class in the scala package, its name, otherwise EmptyTypeName */
781802
def scalaClassName(ref: Type)(implicit ctx: Context): TypeName = scalaClassName(ref.classSymbol)
782803

@@ -795,24 +816,28 @@ class Definitions {
795816
* - FunctionN for N >= 0
796817
* - ImplicitFunctionN for N > 0
797818
*/
798-
def isFunctionClass(cls: Symbol) = scalaClassName(cls).isFunction
819+
def isFunctionClass(cls: Symbol) =
820+
scalaClassName(cls).isFunction || phantomClassName(cls).isFunction
799821

800822
/** Is an implicit function class.
801823
* - ImplicitFunctionN for N > 0
802824
*/
803-
def isImplicitFunctionClass(cls: Symbol) = scalaClassName(cls).isImplicitFunction
825+
def isImplicitFunctionClass(cls: Symbol) =
826+
scalaClassName(cls).isImplicitFunction || phantomClassName(cls).isImplicitFunction
804827

805828
/** Is a class that will be erased to FunctionXXL
806829
* - FunctionN for N >= 22
807830
* - ImplicitFunctionN for N >= 22
808831
*/
809-
def isXXLFunctionClass(cls: Symbol) = scalaClassName(cls).functionArity > MaxImplementedFunctionArity
832+
def isXXLFunctionClass(cls: Symbol) =
833+
scalaClassName(cls).functionArity > MaxImplementedFunctionArity
810834

811835
/** Is a synthetic function class
812836
* - FunctionN for N > 22
813837
* - ImplicitFunctionN for N > 0
814838
*/
815-
def isSyntheticFunctionClass(cls: Symbol) = scalaClassName(cls).isSyntheticFunction
839+
def isSyntheticFunctionClass(cls: Symbol) =
840+
scalaClassName(cls).isSyntheticScalaFunction || phantomClassName(cls).isFunction
816841

817842
def isAbstractFunctionClass(cls: Symbol) = isVarArityClass(cls, str.AbstractFunction)
818843
def isTupleClass(cls: Symbol) = isVarArityClass(cls, str.Tuple)
@@ -829,6 +854,7 @@ class Definitions {
829854
val arity = scalaClassName(cls).functionArity
830855
if (arity > 22) FunctionXXLClass
831856
else if (arity >= 0) FunctionClass(arity)
857+
else if (phantomClassName(cls).isFunction) FunctionClass(0)
832858
else NoSymbol
833859
}
834860

@@ -841,8 +867,9 @@ class Definitions {
841867
*/
842868
def erasedFunctionType(cls: Symbol): Type = {
843869
val arity = scalaClassName(cls).functionArity
844-
if (arity > 22) defn.FunctionXXLType
845-
else if (arity >= 0) defn.FunctionType(arity)
870+
if (arity > 22) FunctionXXLType
871+
else if (arity >= 0) FunctionType(arity)
872+
else if (phantomClassName(cls).isFunction) FunctionType(0)
846873
else NoType
847874
}
848875

@@ -887,7 +914,10 @@ class Definitions {
887914
* trait gets screwed up. Therefore, it is mandatory that FunctionXXL
888915
* is treated as a NoInit trait.
889916
*/
890-
lazy val NoInitClasses = NotRuntimeClasses + FunctionXXLClass
917+
private lazy val NoInitClasses = NotRuntimeClasses + FunctionXXLClass
918+
919+
def isNoInitClass(cls: Symbol): Boolean =
920+
cls.is(NoInitsTrait) || NoInitClasses.contains(cls) || isFunctionClass(cls)
891921

892922
def isPolymorphicAfterErasure(sym: Symbol) =
893923
(sym eq Any_isInstanceOf) || (sym eq Any_asInstanceOf)
@@ -908,7 +938,11 @@ class Definitions {
908938
def isFunctionType(tp: Type)(implicit ctx: Context) = {
909939
val arity = functionArity(tp)
910940
val sym = tp.dealias.typeSymbol
911-
arity >= 0 && isFunctionClass(sym) && tp.isRef(FunctionType(arity, sym.name.isImplicitFunction).typeSymbol)
941+
def top =
942+
if (!sym.owner.derivesFrom(defn.PhantomClass)) defn.AnyType
943+
else sym.owner.thisType.select(tpnme.Any)
944+
def funType = FunctionType(arity, sym.name.isImplicitFunction, top)
945+
arity >= 0 && isFunctionClass(sym) && tp.isRef(funType.typeSymbol)
912946
}
913947

914948
def functionArity(tp: Type)(implicit ctx: Context) = tp.dealias.argInfos.length - 1
@@ -1024,6 +1058,10 @@ class Definitions {
10241058

10251059
lazy val PhantomClass: ClassSymbol = {
10261060
val cls = completeClass(enterCompleteClassSymbol(ScalaPackageClass, tpnme.Phantom, NoInitsTrait, List(AnyType)))
1061+
cls.unforcedDecls.openForMutations.useSynthesizer { name => ctx =>
1062+
if (name.isTypeName && name.isFunction) newFunctionNTrait(name.asTypeName, cls)
1063+
else NoSymbol
1064+
}
10271065

10281066
val any = enterCompleteClassSymbol(cls, tpnme.Any, Protected | Final | NoInitsTrait, Nil)
10291067
val nothing = enterCompleteClassSymbol(cls, tpnme.Nothing, Protected | Final | NoInitsTrait, List(any.typeRef))
@@ -1044,4 +1082,16 @@ class Definitions {
10441082

10451083
def ErasedPhantom_UNIT(implicit ctx: Context) = ErasedPhantomClass.linkedClass.requiredValue("UNIT")
10461084

1085+
/** Ensure that `tp2`' is in the same universe as `tp1`. If that's the case, return
1086+
* `op` applied to both types.
1087+
* If not, issue an error and return `tp1`'.
1088+
*/
1089+
def inSameUniverse(op: (Type, Type) => Type, tp1: Type, tp2: Type, relationship: => String, pos: Position)(implicit ctx: Context): Type =
1090+
if (tp1.topType == tp2.topType)
1091+
op(tp1, tp2)
1092+
else {
1093+
ctx.error(ex"$tp1 and $tp2 are in different universes. They cannot be combined in $relationship", pos)
1094+
tp1
1095+
}
1096+
10471097
}

compiler/src/dotty/tools/dotc/core/NameOps.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ object NameOps {
166166
}
167167
}
168168

169-
/** Is a synthetic function name
169+
/** Is a function name
170170
* - N for FunctionN
171171
* - N for ImplicitFunctionN (N >= 1)
172172
* - (-1) otherwise
@@ -190,12 +190,12 @@ object NameOps {
190190
*/
191191
def isImplicitFunction: Boolean = functionArityFor(str.ImplicitFunction) >= 1
192192

193-
/** Is a synthetic function name
193+
/** Is a synthetic function name (in scala package)
194194
* - FunctionN for N > 22
195195
* - ImplicitFunctionN for N >= 1
196196
* - false otherwise
197197
*/
198-
def isSyntheticFunction: Boolean = {
198+
def isSyntheticScalaFunction: Boolean = {
199199
functionArityFor(str.Function) > MaxImplementedFunctionArity ||
200200
functionArityFor(str.ImplicitFunction) >= 1
201201
}

compiler/src/dotty/tools/dotc/transform/Mixin.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ class Mixin extends MiniPhaseTransform with SymTransformer { thisTransform =>
178178
case Some(call) =>
179179
if (defn.NotRuntimeClasses.contains(baseCls)) Nil else call :: Nil
180180
case None =>
181-
if (baseCls.is(NoInitsTrait) || defn.NoInitClasses.contains(baseCls)) Nil
181+
if (defn.isNoInitClass(baseCls)) Nil
182182
else {
183183
//println(i"synth super call ${baseCls.primaryConstructor}: ${baseCls.primaryConstructor.info}")
184184
transformFollowingDeep(superRef(baseCls.primaryConstructor).appliedToNone) :: Nil

compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -465,10 +465,10 @@ trait TypeAssigner {
465465
tree.withType(ref.tpe)
466466

467467
def assignType(tree: untpd.AndTypeTree, left: Tree, right: Tree)(implicit ctx: Context) =
468-
tree.withType(inSameUniverse(AndType(_, _), left.tpe, right, "an `&`"))
468+
tree.withType(defn.inSameUniverse(AndType(_, _), left.tpe, right.tpe, "an `&`", right.pos))
469469

470470
def assignType(tree: untpd.OrTypeTree, left: Tree, right: Tree)(implicit ctx: Context) =
471-
tree.withType(inSameUniverse(OrType(_, _), left.tpe, right, "an `|`"))
471+
tree.withType(defn.inSameUniverse(OrType(_, _), left.tpe, right.tpe, "an `|`", right.pos))
472472

473473
/** Assign type of RefinedType.
474474
* Refinements are typed as if they were members of refinement class `refineCls`.
@@ -503,7 +503,7 @@ trait TypeAssigner {
503503
def assignType(tree: untpd.TypeBoundsTree, lo: Tree, hi: Tree)(implicit ctx: Context) =
504504
tree.withType(
505505
if (lo eq hi) TypeAlias(lo.tpe)
506-
else inSameUniverse(TypeBounds(_, _), lo.tpe, hi, "type bounds"))
506+
else defn.inSameUniverse(TypeBounds(_, _), lo.tpe, hi.tpe, "type bounds", hi.pos))
507507

508508
def assignType(tree: untpd.Bind, sym: Symbol)(implicit ctx: Context) =
509509
tree.withType(NamedType(NoPrefix, sym))
@@ -547,21 +547,9 @@ trait TypeAssigner {
547547
def assignType(tree: untpd.PackageDef, pid: Tree)(implicit ctx: Context) =
548548
tree.withType(pid.symbol.termRef)
549549

550-
/** Ensure that `tree2`'s type is in the same universe as `tree1`. If that's the case, return
551-
* `op` applied to both types.
552-
* If not, issue an error and return `tree1`'s type.
553-
*/
554-
private def inSameUniverse(op: (Type, Type) => Type, tp1: Type, tree2: Tree, relationship: => String)(implicit ctx: Context): Type =
555-
if (tp1.topType == tree2.tpe.topType)
556-
op(tp1, tree2.tpe)
557-
else {
558-
ctx.error(ex"$tp1 and ${tree2.tpe} are in different universes. They cannot be combined in $relationship", tree2.pos)
559-
tp1
560-
}
561-
562550
private def lubInSameUniverse(trees: List[Tree], relationship: => String)(implicit ctx: Context): Type =
563551
trees match {
564-
case first :: rest => (first.tpe /: rest)(inSameUniverse(_ | _, _, _, relationship))
552+
case first :: rest => (first.tpe /: rest)((tp, tree) => defn.inSameUniverse(_ | _, tp, tree.tpe, relationship, tree.pos))
565553
case Nil => defn.NothingType
566554
}
567555
}

compiler/src/dotty/tools/dotc/typer/Typer.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -702,9 +702,9 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit
702702
else true
703703
case _ => false
704704
}
705-
val funCls = defn.FunctionClass(args.length, isImplicit)
706-
typed(cpy.AppliedTypeTree(tree)(
707-
untpd.TypeTree(funCls.typeRef), args :+ body), pt)
705+
val argsTypes = args.map(tr => typed(tr).tpe)
706+
val funTpe = defn.FunctionType(argsTypes, typed(body).tpe, isImplicit)
707+
typed(cpy.AppliedTypeTree(tree)(untpd.TypeTree(funTpe), args :+ body), pt)
708708
}
709709
else {
710710
val params = args.asInstanceOf[List[untpd.ValDef]]

library/src/scala/Phantom.scala

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,20 @@ trait Phantom {
88
protected final trait Nothing extends this.Any
99
1010
protected final def assume: this.Nothing
11+
12+
trait Function1[-T1 <: this.Any, +R] {
13+
def apply(x1: T1): R
14+
}
15+
trait ImplicitFunction1[-T1 <: this.Any, +R] extends Function1[T1, R] {
16+
/*implicit*/ def apply(x1: T1): R
17+
}
18+
...
19+
trait FunctionN[-T1 <: this.Any, ..., -Tn <: this.Any, +R] {
20+
def apply(x1: T1, ..., xn: Tn): R
21+
}
22+
trait ImplicitFunctionN[-T1 <: this.Any, ..., -Tn <: this.Any, +R] extends FunctionN[T1, ..., Tn, R] {
23+
/*implicit*/ def apply(x1: T1, ..., xn: Tn): R
24+
}
25+
1126
}
1227
*/

tests/neg/phantom-Functions-1.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
class PhantomFun1NoApply extends Function1[Boo.Casper, Unit] // error: class PhantomFun1NoApply needs to be abstract, since def apply: (p0: Casper)Unit is not defined
3+
4+
object Boo extends Phantom {
5+
type Casper <: this.Any
6+
}

tests/neg/phantom-Functions-2.scala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
class PhantomFun1 extends Boo.Function2[Boo.Casper, Int, Unit] { // error: Type argument Int does not conform to upper bound Boo.Any
3+
def apply(x1: Boo.Casper, x2: Int): Unit = ()
4+
}
5+
6+
class PhantomFun2 extends Boo.Function2[Int, Boo.Casper, Unit] { // error: Type argument Int does not conform to upper bound Boo.Any
7+
def apply(x1: Boo.Casper, x2: Int): Unit = ()
8+
}
9+
10+
class Fun extends Function2[Int, Int, Unit] {
11+
def apply(x1: Int, x2: Int): Unit = ()
12+
}
13+
14+
object Boo extends Phantom {
15+
type Casper <: this.Any
16+
}

tests/neg/phantom-Functions-3.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
// TODO: Importing an object that exends phantom makes FunctionN refere to the Boo.FunctionN
3+
// We should be carefull with this. Use a waring when importing Boo._ or disallow it.
4+
// Or put funtions in an inner object in the Phantom trait, for example scala.Phantom.Functions
5+
6+
import Boo._
7+
8+
class Fun extends Function1[Int, Unit] { // error: Type argument Int does not conform to upper bound Boo.Any
9+
def apply(x1: Int): Unit = ()
10+
}
11+
12+
object Boo extends Phantom {
13+
type Casper <: this.Any
14+
}

tests/neg/phantom-Lambda.scala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
class Fun {
3+
def a = (x1: Int, x2: Boo.B) => x1 // error: Int and Boo.B are in different universes. They cannot be combined in function arguments.
4+
def b: (Int, Boo.B) => Unit = (x1, x2) => x1 // error: Int and Boo.B are in different universes. They cannot be combined in function arguments.
5+
6+
def c = (x1: Foo.F, x2: Boo.B) => x1 // error: Foo.F and Boo.B are in different universes. They cannot be combined in function arguments.
7+
def d: (Foo.F, Boo.B) => Unit = (x1, x2) => x1 // error: Foo.F and Boo.B are in different universes. They cannot be combined in function arguments.
8+
}
9+
10+
object Boo extends Phantom {
11+
type B <: this.Any
12+
}
13+
14+
object Foo extends Phantom {
15+
type F <: this.Any
16+
}

0 commit comments

Comments
 (0)