Skip to content

Add TypeOf (first step towards transparent methods) #4671

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

Closed
wants to merge 36 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1fa329a
Add `TypeOf` type representation and corresponding case in `TypeMap`
gsps Jun 6, 2018
cad6a10
Add TypeOfTypeTree and update Parser accordingly
OlivierBlanvillain Jun 6, 2018
42eed69
Merge TypeOfTypeTree and SingletonTypeTree
gsps Jun 13, 2018
d61e3ed
Handle TypeOf in pickler
OlivierBlanvillain Jun 6, 2018
bbdbfb5
Handle TypeOf in TypeAssigner
OlivierBlanvillain Jun 13, 2018
75ace99
Add structural equality for `TypeOf`s
gsps Jun 13, 2018
d188933
Add missing case in sameTree
OlivierBlanvillain Jun 12, 2018
9767a70
Detect transparency when typing if and match
OlivierBlanvillain Jun 13, 2018
649aa98
Make TypeOf a subclass of AnnotatedType
OlivierBlanvillain Jun 13, 2018
e55c92f
Override derivedAnnotatedType
OlivierBlanvillain Jun 13, 2018
d3161ef
Swap tree and underlying in TypeOf to match Annot
OlivierBlanvillain Jun 13, 2018
91c72bf
Decompose precise protos during typing
OlivierBlanvillain Jun 13, 2018
8a9e6a1
Avoid infinite loop when printing TypeOf
gsps Jun 13, 2018
116e9bd
Also decompose proto for matches
OlivierBlanvillain Jun 13, 2018
82c3dea
Add tests
OlivierBlanvillain Jun 13, 2018
f3c9235
Implement `derivedTypeOf` in `ApproximatingTypeMap`
gsps Jun 13, 2018
2d4a926
fixup! Avoid infinite loop when printing TypeOf
OlivierBlanvillain Jun 13, 2018
6289f21
TypeOf shouldn't be handled by AnnotatedType logic
OlivierBlanvillain Jun 13, 2018
93fa77a
fixup! Avoid infinite loop when printing TypeOf
OlivierBlanvillain Jun 13, 2018
32d9858
Update TypeMap to use assignTypes
OlivierBlanvillain Jun 13, 2018
3f9d6e6
Fix `TypeMap` and implement `TypeAccumulator` for `TypeOf`
gsps Jun 14, 2018
fed6013
Fix pretty printing of `TypeOf`
gsps Jun 14, 2018
367a44a
Fix `isInTypeOf` state used for pretty printing `TypeOf`s
gsps Jun 14, 2018
a395742
Add orphan check for underlying types of `TypeOf`s
gsps Jun 14, 2018
84cc5bf
Fix pretty printing of `TypeOf`s to reveal types, not trees (which mi…
gsps Jun 14, 2018
705f864
WIP Fixing duplicate TypeOf
gsps Jun 14, 2018
169fa74
Change trees in TypeOf to never loop back to their TypeOf
gsps Jun 14, 2018
43d4d74
Remove TypeOf.fromUntyped
OlivierBlanvillain Jun 15, 2018
a2ebbc4
Create TypeOfAnnot in .pply to prevent ctx capture
OlivierBlanvillain Jun 15, 2018
1198a86
Factor TypeOf and Tree printers
OlivierBlanvillain Jun 15, 2018
004a69c
Minor comment updates
OlivierBlanvillain Jun 15, 2018
4fb78f3
Remove derivedTypeOf as it's never used
OlivierBlanvillain Jun 15, 2018
f200e00
Document TypeOf class
OlivierBlanvillain Jun 15, 2018
bfbfadd
Remove non-sensical example in TypeMap documentation
gsps Jun 15, 2018
f5539bf
Add missing This and Super in assignType(SingletonTypeTree)
OlivierBlanvillain Jun 15, 2018
27b1690
Fix name clash betwen TypeOf and dotty.tools.repl.TypeOf
OlivierBlanvillain Jun 15, 2018
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
5 changes: 3 additions & 2 deletions compiler/src/dotty/tools/dotc/ast/Trees.scala
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,9 @@ object Trees {
case y: List[_] => x.corresponds(y)(isSame)
case _ => false
}
case x: Constant => x == y
case _ =>
false
throw new AssertionError(s"Unexpected Tree in Tree comparison $x (comparing to $y)")
}
}
this.getClass == that.getClass && {
Expand Down Expand Up @@ -605,7 +606,7 @@ object Trees {
*/
class TypeVarBinder[-T >: Untyped] extends TypeTree[T]

/** ref.type */
/** ref.type or { ref } */
case class SingletonTypeTree[-T >: Untyped] private[ast] (ref: Tree[T])
extends DenotingTree[T] with TypTree[T] {
type ThisTree[-T >: Untyped] = SingletonTypeTree[T]
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/ast/tpd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
override def If(tree: Tree)(cond: Tree, thenp: Tree, elsep: Tree)(implicit ctx: Context): If = {
val tree1 = untpd.cpy.If(tree)(cond, thenp, elsep)
tree match {
case tree: If if (thenp.tpe eq tree.thenp.tpe) && (elsep.tpe eq tree.elsep.tpe) => tree1.withTypeUnchecked(tree.tpe)
case tree: If if (cond.tpe eq tree.cond.tpe) && (thenp.tpe eq tree.thenp.tpe) && (elsep.tpe eq tree.elsep.tpe) => tree1.withTypeUnchecked(tree.tpe)
case _ => ta.assignType(tree1, thenp, elsep)
}
}
Expand All @@ -557,7 +557,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
override def Match(tree: Tree)(selector: Tree, cases: List[CaseDef])(implicit ctx: Context): Match = {
val tree1 = untpd.cpy.Match(tree)(selector, cases)
tree match {
case tree: Match if sameTypes(cases, tree.cases) => tree1.withTypeUnchecked(tree.tpe)
case tree: Match if (selector.tpe eq tree.selector.tpe) && sameTypes(cases, tree.cases) => tree1.withTypeUnchecked(tree.tpe)
case _ => ta.assignType(tree1, cases)
}
}
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,8 @@ class Definitions {
def ThrowsAnnot(implicit ctx: Context) = ThrowsAnnotType.symbol.asClass
lazy val TransientAnnotType = ctx.requiredClassRef("scala.transient")
def TransientAnnot(implicit ctx: Context) = TransientAnnotType.symbol.asClass
lazy val TypeOfAnnotType = ctx.requiredClassRef("scala.annotation.internal.TypeOf")
def TypeOfAnnot(implicit ctx: Context) = TypeOfAnnotType.symbol.asClass
lazy val UncheckedAnnotType = ctx.requiredClassRef("scala.unchecked")
def UncheckedAnnot(implicit ctx: Context) = UncheckedAnnotType.symbol.asClass
lazy val UncheckedStableAnnotType = ctx.requiredClassRef("scala.annotation.unchecked.uncheckedStable")
Expand Down
9 changes: 9 additions & 0 deletions compiler/src/dotty/tools/dotc/core/SymDenotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,15 @@ object SymDenotations {
def isTransparentMethod(implicit ctx: Context): Boolean =
is(TransparentMethod, butNot = Accessor)

/** Are we in a transparent context?
* Either - this symbol has a transparent owner, itself included
* Or - we are inside a SingletonTypeTree
*/
def isTransitivelyTransparent(implicit ctx: Context): Boolean = {
// Note: Should we use a mode instead?
ownersIterator.exists(_.isTransparentMethod) || ctx.outersIterator.exists(_.tree.isInstanceOf[SingletonTypeTree[_]])
}

def isInlineableMethod(implicit ctx: Context) = isInlinedMethod || isTransparentMethod

/** ()T and => T types should be treated as equivalent for this symbol.
Expand Down
4 changes: 3 additions & 1 deletion compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ class TypeComparer(initctx: Context) extends DotClass with ConstraintHandling {
compareWild
case tp2: LazyRef =>
!tp2.evaluating && recur(tp1, tp2.ref)
case tp2: TypeOf =>
tp2 == tp1 || secondTry
case tp2: AnnotatedType if !tp2.isRefining =>
recur(tp1, tp2.parent)
case tp2: ThisType =>
Expand Down Expand Up @@ -567,7 +569,7 @@ class TypeComparer(initctx: Context) extends DotClass with ConstraintHandling {
false
}
compareTypeBounds
case tp2: AnnotatedType if tp2.isRefining =>
case tp2: AnnotatedType if tp2.isRefining && !tp2.isInstanceOf[TypeOf] =>
(tp1.derivesAnnotWith(tp2.annot.sameAnnotation) || defn.isBottomType(tp1)) &&
recur(tp1, tp2.parent)
case ClassInfo(pre2, cls2, _, _, _) =>
Expand Down
142 changes: 140 additions & 2 deletions compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import util.Stats._
import util.{DotClass, SimpleIdentitySet}
import reporting.diagnostic.Message
import ast.tpd._
import ast.TreeTypeMap
import ast.{Trees, TreeTypeMap}
import printing.Texts._
import ast.untpd
import dotty.tools.dotc.transform.Erasure
Expand Down Expand Up @@ -3707,7 +3707,7 @@ object Types {

override def underlying(implicit ctx: Context): Type = parent

def derivedAnnotatedType(parent: Type, annot: Annotation) =
def derivedAnnotatedType(parent: Type, annot: Annotation)(implicit ctx: Context) =
if ((parent eq this.parent) && (annot eq this.annot)) this
else AnnotatedType(parent, annot)

Expand Down Expand Up @@ -3929,6 +3929,100 @@ object Types {
else None
}

// ----- TypeOf -------------------------------------------------------------------------

/** Type that represents the precise type of a given term.
* Precision is only kept for Apply, TypeApply, If and Match trees.
*
* The idea behind this type is to be able to compute more precise types
* when more information is available.
*
* TypeOfs are represented by an underlying type and a tree. The top level
* node of the tree must be one of the nodes mentioned above, and is only
* used as a "marker" node, meaning that we will never look at its type.
*
* In a sense, TypeOf types are isomorphic to the following 4 types:
*
* TypeOf(u, Apply(fun, args)) ~ SuspendedApply(u, fun, args)
* TypeOf(u, TypeApply(fun, args)) ~ SuspendedTypeApply(u, fun, args)
* TypeOf(u, If(cond, thenp, elsep)) ~ SuspendedIf(u, cond, thenp, elsep)
* TypeOf(u, Match(selector, cases)) ~ SuspendedMatch(u, selector, cases)
*
* Where u is the type that the tree would have had otherwise.
*
* It should be the case that whenever two TypeOfs are equal, so are their
* underlying types.
*/
class TypeOf private (val underlyingTp: Type, val tree: Tree, annot: Annotation) extends AnnotatedType(underlyingTp, annot) {
assert(TypeOf.isLegalTopLevelTree(tree), s"Illegal top-level tree in TypeOf: $tree")

override def equals(that: Any): Boolean = {
that match {
case that: TypeOf =>
def compareTree(tree1: Tree, tree2: Tree): Boolean = {
def compareArgs[T <: Tree](args1: List[T], args2: List[T]): Boolean =
args1.zip(args2).forall { case (a,b) => a.tpe == b.tpe }
(tree1, tree2) match {
case (t1: Apply, t2: Apply) =>
t1.fun.tpe == t2.fun.tpe && compareArgs(t1.args, t2.args)
case (t1: TypeApply, t2: TypeApply) =>
t1.fun.tpe == t2.fun.tpe && compareArgs(t1.args, t2.args)
case (t1: If, t2: If) =>
t1.cond.tpe == t2.cond.tpe && t1.thenp.tpe == t2.thenp.tpe && t1.elsep.tpe == t2.elsep.tpe
case (t1: Match, t2: Match) =>
t1.selector.tpe == t2.selector.tpe && compareArgs(t1.cases, t2.cases)
case (t1, t2) =>
false
}
}
compareTree(this.tree, that.tree)
case _ => false
}
}

override def derivedAnnotatedType(parent: Type, annot: Annotation)(implicit ctx: Context): AnnotatedType =
if ((parent eq this.parent) && (annot eq this.annot)) this
else TypeOf(parent, annot.arguments.head)

override def toString(): String = s"TypeOf($underlyingTp, $tree)"
}

object TypeOf {
def apply(underlyingTp: Type, tree: untpd.Tree)(implicit ctx: Context): TypeOf = {
val tree1 = tree.clone.asInstanceOf[Tree]
// This is a safety net to keep us from touching a TypeOf's tree's type.
// Assuming we never look at this type, it would be safe to simply reuse
// tree without cloning. The invariant is currently enforced in Ycheck.
// To disable this safety net we will also have to update the pickler
// to ignore the type of the TypeOf tree's.
tree1.overwriteType(NoType)
new TypeOf(underlyingTp, tree1, Annotation(defn.TypeOfAnnot, tree1))
}
def unapply(to: TypeOf): Option[(Type, Tree)] = Some((to.underlyingTp, to.tree))

def isLegalTopLevelTree(tree: Tree): Boolean = tree match {
case _: TypeApply | _: Apply | _: If | _: Match => true
case _ => false
}

object If {
def unapply(to: TypeOf): Option[(Type, Type, Type)] = to.tree match {
case Trees.If(cond, thenb, elseb) => Some((cond.tpe, thenb.tpe, elseb.tpe))
case _ => None
}
}

object Match {
def unapply(to: TypeOf): Option[(Type, List[Type])] = to.tree match {
case Trees.Match(cond, cases) =>
// TODO: We only look at .body.tpe for now, eventually we should
// also take the guard and the pattern into account.
Some((cond.tpe, cases.map(_.body.tpe)))
case _ => None
}
}
}

// ----- TypeMaps --------------------------------------------------------------------

/** Common base class of TypeMap and TypeAccumulator */
Expand Down Expand Up @@ -4069,6 +4163,30 @@ object Types {
case tp: SkolemType =>
tp

case tp: TypeOf =>
def copyMapped[ThisTree <: Tree](tree: ThisTree): ThisTree = {
val tp1 = this(tree.tpe)
if (tree.tpe eq tp1) tree else tree.withTypeUnchecked(tp1).asInstanceOf[ThisTree]
}
val tree1 = tp.tree match {
case tree: TypeApply =>
cpy.TypeApply(tree)(copyMapped(tree.fun), tree.args.mapConserve(copyMapped))
case tree: Apply =>
cpy.Apply(tree)(copyMapped(tree.fun), tree.args.mapConserve(copyMapped))
case tree: If =>
cpy.If(tree)(copyMapped(tree.cond), copyMapped(tree.thenp), copyMapped(tree.elsep))
case tree: Match =>
cpy.Match(tree)(copyMapped(tree.selector), tree.cases.mapConserve(copyMapped))
case tree =>
throw new AssertionError(s"TypeOf shouldn't contain $tree as top-level node.")
}
if (tp.tree ne tree1) {
assert(!tp.underlyingTp.exists || tree1.tpe.exists, i"Derived TypeOf's type became NoType")
tree1.tpe
} else {
tp
}

case tp @ AnnotatedType(underlying, annot) =>
val underlying1 = this(underlying)
if (underlying1 eq underlying) tp
Expand Down Expand Up @@ -4333,6 +4451,7 @@ object Types {
if (underlying.isBottomType) underlying
else tp.derivedAnnotatedType(underlying, annot)
}

override protected def derivedWildcardType(tp: WildcardType, bounds: Type) = {
tp.derivedWildcardType(rangeToBounds(bounds))
}
Expand Down Expand Up @@ -4440,6 +4559,25 @@ object Types {
case tp: OrType =>
this(this(x, tp.tp1), tp.tp2)

case tp: TypeOf =>
@tailrec def foldTrees(x: T, ts: List[Tree]): T = ts match {
case t :: ts1 => foldTrees(apply(x, t.tpe), ts1)
case nil => x
}

tp.tree match {
case tree: TypeApply =>
foldTrees(this(x, tree.fun.tpe), tree.args)
case tree: Apply =>
foldTrees(this(x, tree.fun.tpe), tree.args)
case tree: If =>
this(this(this(x, tree.cond.tpe), tree.thenp.tpe), tree.elsep.tpe)
case tree: Match =>
foldTrees(this(x, tree.selector.tpe), tree.cases)
case tree =>
throw new AssertionError(s"TypeOf shouldn't contain $tree as top-level node.")
}

case AnnotatedType(underlying, annot) =>
this(applyToAnnot(x, annot), underlying)

Expand Down
4 changes: 3 additions & 1 deletion compiler/src/dotty/tools/dotc/core/tasty/TastyFormat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ object TastyFormat {
final val ANNOTATION = 172
final val TERMREFin = 173
final val TYPEREFin = 174
final val TYPEOF = 175

// In binary: 101100EI
// I = implicit method type
Expand Down Expand Up @@ -430,7 +431,7 @@ object TastyFormat {
firstNatTreeTag <= tag && tag <= SYMBOLconst ||
firstASTTreeTag <= tag && tag <= SINGLETONtpt ||
firstNatASTTreeTag <= tag && tag <= NAMEDARG ||
firstLengthTreeTag <= tag && tag <= TYPEREFin ||
firstLengthTreeTag <= tag && tag <= ERASEDIMPLICITMETHODtype ||
tag == HOLE

def isParamTag(tag: Int) = tag == PARAM || tag == TYPEPARAM
Expand Down Expand Up @@ -595,6 +596,7 @@ object TastyFormat {
case SUPERtype => "SUPERtype"
case TERMREFin => "TERMREFin"
case TYPEREFin => "TYPEREFin"
case TYPEOF => "TYPEOF"

case REFINEDtype => "REFINEDtype"
case REFINEDtpt => "REFINEDtpt"
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/core/tasty/TastyPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ class TastyPrinter(bytes: Array[Byte])(implicit ctx: Context) {
until(end) { printName(); printTree() }
case PARAMtype =>
printNat(); printNat()
case TYPEOF =>
printTree(); printTree()
case _ =>
printTrees()
}
Expand Down
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ class TreePickler(pickler: TastyPickler) {
withLength { pickleType(tycon); args.foreach(pickleType(_)) }
case ConstantType(value) =>
pickleConstant(value)
case tpe: TypeOf =>
writeByte(TYPEOF)
withLength { pickleType(tpe.underlyingTp, richTypes); pickleTree(tpe.tree) }
case tpe: NamedType =>
val sym = tpe.symbol
def pickleExternalRef(sym: Symbol) = {
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,8 @@ class TreeUnpickler(reader: TastyReader,
TypeBounds(readType(), readType())
case ANNOTATEDtype =>
AnnotatedType(readType(), Annotation(readTerm()))
case TYPEOF =>
TypeOf(readType(), readTerm())
case ANDtype =>
AndType(readType(), readType())
case ORtype =>
Expand Down
8 changes: 7 additions & 1 deletion compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -870,7 +870,7 @@ object Parsers {
makeTupleOrParens(inParens(argTypes(namedOK = false, wildOK = true)))
}
else if (in.token == LBRACE)
atPos(in.offset) { RefinedTypeTree(EmptyTree, refinement()) }
atPos(in.offset) { inBraces(refinementOnEmptyOrSingleton()) }
else if (isSimpleLiteral) { SingletonTypeTree(literal()) }
else if (in.token == USCORE) {
val start = in.skipToken()
Expand All @@ -884,6 +884,12 @@ object Parsers {
}
}

/** A refinement on an empty tree or a singleton type tree. */
def refinementOnEmptyOrSingleton(): Tree = {
if (!isStatSeqEnd && !isDclIntro) SingletonTypeTree(expr1())
else RefinedTypeTree(EmptyTree, refineStatSeq())
}

val handleSingletonType: Tree => Tree = t =>
if (in.token == TYPE) {
in.nextToken()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,6 @@ class DecompilerPrinter(_ctx: Context) extends RefinedPrinter(_ctx) {
override protected def typeApplyText[T >: Untyped](tree: TypeApply[T]): Text = {
if (tree.symbol eq defn.QuotedExpr_apply) "'"
else if (tree.symbol eq defn.QuotedType_apply) "'[" ~ toTextGlobal(tree.args, ", ") ~ "]"
else super.typeApplyText(tree)
else super.typeApplyText(tree.fun, tree.args)
}
}
23 changes: 19 additions & 4 deletions compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ class PlainPrinter(_ctx: Context) extends Printer {
protected[this] implicit def ctx: Context = _ctx.addMode(Mode.Printing)

private[this] var openRecs: List[RecType] = Nil
protected[this] var isInTypeOf: Boolean = false

protected final def inTypeOf(op: => Text): Text = {
val saved = isInTypeOf
isInTypeOf = true
try { op } finally { isInTypeOf = saved }
}

protected def maxToTextRecursions = 100

Expand Down Expand Up @@ -142,7 +149,10 @@ class PlainPrinter(_ctx: Context) extends Printer {
case tp: TypeParamRef =>
ParamRefNameString(tp) ~ lambdaHash(tp.binder)
case tp: SingletonType =>
toTextLocal(tp.underlying) ~ "(" ~ toTextRef(tp) ~ ")"
if (isInTypeOf)
toTextRef(tp)
else
toTextLocal(tp.underlying) ~ "(" ~ toTextRef(tp) ~ ")"
case AppliedType(tycon, args) =>
(toTextLocal(tycon) ~ "[" ~ Text(args map argText, ", ") ~ "]").close
case tp: RefinedType =>
Expand Down Expand Up @@ -183,6 +193,8 @@ class PlainPrinter(_ctx: Context) extends Printer {
(Str(" => ") provided !tp.resultType.isInstanceOf[MethodType]) ~
toTextGlobal(tp.resultType)
}
case TypeOf(underlyingTp, _) =>
"{ ... <: " ~ toText(underlyingTp) ~ " }"
case AnnotatedType(tpe, annot) =>
toTextLocal(tpe) ~ " " ~ toText(annot)
case tp: TypeVar =>
Expand Down Expand Up @@ -483,9 +495,12 @@ class PlainPrinter(_ctx: Context) extends Printer {
val nodeName = tree.productPrefix
val elems =
Text(tree.productIterator.map(toTextElem).toList, ", ")
val tpSuffix =
if (ctx.settings.XprintTypes.value && tree.hasType)
" | " ~ toText(tree.typeOpt)
val tpSuffix: Text =
if (ctx.settings.XprintTypes.value && tree.hasType && !isInTypeOf)
tree.typeOpt match {
case tp: TypeOf => " | <idem>"
case tp => " | " ~ toText(tree.typeOpt)
}
else
Text()

Expand Down
Loading