From 0041987fdc19922ef0fabfe51f6f5621a9da54cf Mon Sep 17 00:00:00 2001 From: Kacper Korban Date: Thu, 19 Dec 2024 16:21:35 +0100 Subject: [PATCH] Implement `tracked` members (#21761) closes #21754 Allow for the `tracked` modifier to be used for `val` members of classes and traits. `tracked` members and members inheriting from `tracked` force the type of the member (or it's overriding member) to be as exact as possible. More precisely, it will will assign the `tracked` member the infered type of the rhs. For instance, consider the following definition: ```scala 3 trait F: tracked val a: Int tracked val b: Int class N extends F: val a = 22 // a.type =:= 22 val b: Int = 22 // b.type =:= Int tracked val c = 22 // c.type =:= 22 ``` Here, the `tracked` modifier ensures that the type of `a` in `N` is `22` and not `Int`. But the type of `b` is `N` is `Int` since it's explicitly declared as `Int`. `tracked` members can also be immediately initialized, as in the case of `c`. --------- Co-authored-by: Matt Bovel Co-authored-by: odersky --- .../src/dotty/tools/dotc/ast/Desugar.scala | 12 +++- .../src/dotty/tools/dotc/core/Flags.scala | 2 +- .../dotty/tools/dotc/parsing/Parsers.scala | 4 +- .../dotty/tools/dotc/parsing/Scanners.scala | 3 +- .../src/dotty/tools/dotc/typer/Namer.scala | 16 ++++-- .../src/dotty/tools/dotc/typer/Typer.scala | 10 ++-- docs/_docs/internals/syntax.md | 5 +- .../reference/experimental/modularity.md | 40 +++++++++++--- tests/neg/abstract-tracked-1.scala | 12 ++++ tests/neg/abstract-tracked.check | 20 +++++++ tests/neg/abstract-tracked.scala | 14 +++++ tests/neg/tracked.check | 50 ++++++----------- tests/neg/tracked.scala | 6 +- tests/pos/abstract-tracked-2.scala | 11 ++++ tests/pos/abstract-tracked.scala | 55 +++++++++++++++++++ 15 files changed, 198 insertions(+), 62 deletions(-) create mode 100644 tests/neg/abstract-tracked-1.scala create mode 100644 tests/neg/abstract-tracked.check create mode 100644 tests/neg/abstract-tracked.scala create mode 100644 tests/pos/abstract-tracked-2.scala create mode 100644 tests/pos/abstract-tracked.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index a95e64e24b85..67e1885b511f 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1086,12 +1086,13 @@ object desugar { if mods.isAllOf(Given | Inline | Transparent) then report.error("inline given instances cannot be trasparent", cdef) var classMods = if mods.is(Given) then mods &~ (Inline | Transparent) | Synthetic else mods - if vparamAccessors.exists(_.mods.is(Tracked)) then + val newBody = tparamAccessors ::: vparamAccessors ::: normalizedBody ::: caseClassMeths + if newBody.collect { case d: ValOrDefDef => d }.exists(_.mods.is(Tracked)) then classMods |= Dependent cpy.TypeDef(cdef: TypeDef)( name = className, rhs = cpy.Template(impl)(constr, parents1, clsDerived, self1, - tparamAccessors ::: vparamAccessors ::: normalizedBody ::: caseClassMeths) + newBody) ).withMods(classMods) } @@ -1561,6 +1562,12 @@ object desugar { rhsOK(rhs) } + val legalTracked: Context ?=> MemberDefTest = { + case valdef @ ValDef(_, _, _) => + val sym = valdef.symbol + !ctx.owner.exists || ctx.owner.isClass || ctx.owner.is(Case) || ctx.owner.isConstructor || valdef.mods.is(Param) || valdef.mods.is(ParamAccessor) + } + def checkOpaqueAlias(tree: MemberDef)(using Context): MemberDef = def check(rhs: Tree): MemberDef = rhs match case bounds: TypeBoundsTree if bounds.alias.isEmpty => @@ -1586,6 +1593,7 @@ object desugar { } else tested tested = checkOpaqueAlias(tested) tested = checkApplicable(Opaque, legalOpaque) + tested = checkApplicable(Tracked, legalTracked) tested case _ => tree diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index b915373da021..0775b3caaf0c 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -480,7 +480,7 @@ object Flags { */ val AfterLoadFlags: FlagSet = commonFlags( FromStartFlags, AccessFlags, Final, AccessorOrSealed, - Abstract, LazyOrTrait, SelfName, JavaDefined, JavaAnnotation, Transparent, Tracked) + Abstract, LazyOrTrait, SelfName, JavaDefined, JavaAnnotation, Transparent) /** A value that's unstable unless complemented with a Stable flag */ val UnstableValueFlags: FlagSet = Mutable | Method diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index e9f6b01a99c3..7933cbbea12f 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -3519,7 +3519,7 @@ object Parsers { * UsingClsTermParamClause::= ‘(’ ‘using’ [‘erased’] (ClsParams | ContextTypes) ‘)’ * ClsParams ::= ClsParam {‘,’ ClsParam} * ClsParam ::= {Annotation} - * [{Modifier | ‘tracked’} (‘val’ | ‘var’)] Param + * [{Modifier} (‘val’ | ‘var’)] Param * TypelessClause ::= DefTermParamClause * | UsingParamClause * @@ -3557,8 +3557,6 @@ object Parsers { if isErasedKw then mods = addModifier(mods) if paramOwner.isClass then - if isIdent(nme.tracked) && in.featureEnabled(Feature.modularity) && !in.lookahead.isColon then - mods = addModifier(mods) mods = addFlag(modifiers(start = mods), ParamAccessor) mods = if in.token == VAL then diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 2dc0a1a8d805..2007b633a7c5 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -212,6 +212,7 @@ object Scanners { def featureEnabled(name: TermName) = Feature.enabled(name)(using languageImportContext) def erasedEnabled = featureEnabled(Feature.erasedDefinitions) + def trackedEnabled = featureEnabled(Feature.modularity) private var postfixOpsEnabledCache = false private var postfixOpsEnabledCtx: Context = NoContext @@ -1195,7 +1196,7 @@ object Scanners { def isSoftModifier: Boolean = token == IDENTIFIER - && (softModifierNames.contains(name) || name == nme.erased && erasedEnabled) + && (softModifierNames.contains(name) || name == nme.erased && erasedEnabled || name == nme.tracked && trackedEnabled) def isSoftModifierInModifierPosition: Boolean = isSoftModifier && inModifierPosition() diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 61c4f5b294cd..e8b22325d1e9 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -878,7 +878,7 @@ class Namer { typer: Typer => case original: untpd.MemberDef => lazy val annotCtx = annotContext(original, sym) original.setMods: - original.mods.withAnnotations : + original.mods.withAnnotations: original.mods.annotations.mapConserve: annotTree => val cls = typedAheadAnnotationClass(annotTree)(using annotCtx) if (cls eq sym) @@ -2017,6 +2017,11 @@ class Namer { typer: Typer => paramFn: Type => Type, fallbackProto: Type )(using Context): Type = + /** Is this member tracked? This is true if it is marked as `tracked` or if + * it overrides a `tracked` member. To account for the later, `isTracked` + * is overriden to `true` as a side-effect of computing `inherited`. + */ + var isTracked: Boolean = sym.is(Tracked) /** A type for this definition that might be inherited from elsewhere: * If this is a setter parameter, the corresponding getter type. @@ -2052,8 +2057,10 @@ class Namer { typer: Typer => if paramss.isEmpty then info.widenExpr else NoType - val iRawInfo = - cls.info.nonPrivateDecl(sym.name).matchingDenotation(site, schema, sym.targetName).info + val iDenot = cls.info.nonPrivateDecl(sym.name).matchingDenotation(site, schema, sym.targetName) + val iSym = iDenot.symbol + if iSym.is(Tracked) then isTracked = true + val iRawInfo = iDenot.info val iResType = instantiatedResType(iRawInfo, paramss).asSeenFrom(site, cls) if (iResType.exists) typr.println(i"using inherited type for ${mdef.name}; raw: $iRawInfo, inherited: $iResType") @@ -2147,6 +2154,7 @@ class Namer { typer: Typer => if defaultTp.exists then TypeOps.SimplifyKeepUnchecked() else null) match case ctp: ConstantType if sym.isInlineVal => ctp + case tp if isTracked => tp case tp => TypeComparer.widenInferred(tp, pt, Widen.Unions) // Replace aliases to Unit by Unit itself. If we leave the alias in @@ -2157,7 +2165,7 @@ class Namer { typer: Typer => def lhsType = fullyDefinedType(cookedRhsType, "right-hand side", mdef.srcPos) //if (sym.name.toString == "y") println(i"rhs = $rhsType, cooked = $cookedRhsType") if (inherited.exists) - if sym.isInlineVal then lhsType else inherited + if sym.isInlineVal || isTracked then lhsType else inherited else { if (sym.is(Implicit)) mdef match { diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index d15e263ce777..c941ffe74e18 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2433,7 +2433,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer else if ctx.reporter.errorsReported then UnspecifiedErrorType else errorType(em"cannot infer type; expected type $pt is not fully defined", tree.srcPos)) - def typedTypeTree(tree: untpd.TypeTree, pt: Type)(using Context): Tree = + def typedTypeTree(tree: untpd.TypeTree, pt: Type)(using Context): Tree = { tree match case tree: untpd.DerivedTypeTree => tree.ensureCompletions @@ -2449,6 +2449,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } case _ => completeTypeTree(InferredTypeTree(), pt, tree) + } def typedInLambdaTypeTree(tree: untpd.InLambdaTypeTree, pt: Type)(using Context): Tree = val tp = @@ -2860,7 +2861,6 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val nnInfo = rhs1.notNullInfo vdef1.withNotNullInfo(if sym.is(Lazy) then nnInfo.retractedInfo else nnInfo) } - private def retractDefDef(sym: Symbol)(using Context): Tree = // it's a discarded method (synthetic case class method or synthetic java record constructor or overridden member), drop it val canBeInvalidated: Boolean = @@ -3672,7 +3672,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } /** Typecheck and adapt tree, returning a typed tree. Parameters as for `typedUnadapted` */ - def typed(tree: untpd.Tree, pt: Type, locked: TypeVars)(using Context): Tree = + def typed(tree: untpd.Tree, pt: Type, locked: TypeVars)(using Context): Tree = { trace(i"typing $tree, pt = $pt", typr, show = true) { record(s"typed $getClass") record("typed total") @@ -3684,6 +3684,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer tree.withType(WildcardType) else adapt(typedUnadapted(tree, pt, locked), pt, locked) } + } def typed(tree: untpd.Tree, pt: Type = WildcardType)(using Context): Tree = typed(tree, pt, ctx.typerState.ownedVars) @@ -3799,7 +3800,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def typedExpr(tree: untpd.Tree, pt: Type = WildcardType)(using Context): Tree = withoutMode(Mode.PatternOrTypeBits)(typed(tree, pt)) - def typedType(tree: untpd.Tree, pt: Type = WildcardType, mapPatternBounds: Boolean = false)(using Context): Tree = + def typedType(tree: untpd.Tree, pt: Type = WildcardType, mapPatternBounds: Boolean = false)(using Context): Tree = { val tree1 = withMode(Mode.Type) { typed(tree, pt) } if mapPatternBounds && ctx.mode.is(Mode.Pattern) && !ctx.isAfterTyper then tree1 match @@ -3815,6 +3816,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case tree1 => tree1 else tree1 + } def typedPattern(tree: untpd.Tree, selType: Type = WildcardType)(using Context): Tree = withMode(Mode.Pattern)(typed(tree, selType)) diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index d0074bb503c2..665b4f5144ba 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -141,7 +141,7 @@ type val var while with yield ### Soft keywords ``` -as derives end erased extension infix inline opaque open throws transparent using | * + - +as derives end erased extension infix inline opaque open throws tracked transparent using | * + - ``` See the [separate section on soft keywords](../reference/soft-modifier.md) for additional @@ -381,7 +381,7 @@ ClsParamClause ::= [nl] ‘(’ ClsParams ‘)’ | [nl] ‘(’ ‘using’ (ClsParams | FunArgTypes) ‘)’ ClsParams ::= ClsParam {‘,’ ClsParam} ClsParam ::= {Annotation} ValDef(mods, id, tpe, expr) -- point of mods on val/var - [{Modifier | ‘tracked’} (‘val’ | ‘var’)] Param + [{Modifier} (‘val’ | ‘var’)] Param DefParamClauses ::= DefParamClause { DefParamClause } -- and two DefTypeParamClause cannot be adjacent DefParamClause ::= DefTypeParamClause @@ -418,6 +418,7 @@ LocalModifier ::= ‘abstract’ | ‘transparent’ | ‘infix’ | ‘erased’ + | ‘tracked’ AccessModifier ::= (‘private’ | ‘protected’) [AccessQualifier] AccessQualifier ::= ‘[’ id ‘]’ diff --git a/docs/_docs/reference/experimental/modularity.md b/docs/_docs/reference/experimental/modularity.md index a989b71770af..66d4c0c23ede 100644 --- a/docs/_docs/reference/experimental/modularity.md +++ b/docs/_docs/reference/experimental/modularity.md @@ -108,14 +108,6 @@ This works as it should now. Without the addition of `tracked` to the parameter of `SetFunctor` typechecking would immediately lose track of the element type `T` after an `add`, and would therefore fail. -**Syntax Change** - -``` -ClsParam ::= {Annotation} [{Modifier | ‘tracked’} (‘val’ | ‘var’)] Param -``` - -The (soft) `tracked` modifier is only allowed for `val` parameters of classes. - **Discussion** Since `tracked` is so useful, why not assume it by default? First, `tracked` makes sense only for `val` parameters. If a class parameter is not also a field declared using `val` then there's nothing to refine in the constructor result type. One could think of at least making all `val` parameters tracked by default, but that would be a backwards incompatible change. For instance, the following code would break: @@ -134,6 +126,38 @@ only if the class refers to a type member of `x`. But it turns out that this scheme is unimplementable since it would quickly lead to cyclic references when typechecking recursive class graphs. So an explicit `tracked` looks like the best available option. +## Tracked members + +The `tracked` modifier can also be used for `val` members of classes and traits +to force the type of the member (or it's overriding member) to be as exact as +possible. More precisely, it will will assign the `tracked` member the infered +type of the rhs. For instance, consider the following definition: + +```scala +trait F: + tracked val a: Int + tracked val b: Int + +class N extends F: + val a = 22 // a.type =:= 22 + val b: Int = 22 // b.type =:= Int + tracked val c = 22 // c.type =:= 22 +``` + +Here, the `tracked` modifier ensures that the type of `a` in `N` is `22` and not +`Int`. But the type of `b` is `N` is `Int` since it's explicitly declared as +`Int`. `tracked` members can also be immediately initialized, as in the case of +`c`. + +## Tracked syntax change + +``` +LocalModifier ::= ‘tracked’ +``` + +The (soft) `tracked` modifier is allowed as a local modifier. + + ## Allow Class Parents to be Refined Types Since `tracked` parameters create refinements in constructor types, diff --git a/tests/neg/abstract-tracked-1.scala b/tests/neg/abstract-tracked-1.scala new file mode 100644 index 000000000000..0aef9f938816 --- /dev/null +++ b/tests/neg/abstract-tracked-1.scala @@ -0,0 +1,12 @@ +import scala.language.experimental.modularity +import scala.language.future + +trait F: + tracked val a: Int + +class G: + val a: Int = 1 + +def Test = + val g = new G + summon[g.a.type <:< 1] // error diff --git a/tests/neg/abstract-tracked.check b/tests/neg/abstract-tracked.check new file mode 100644 index 000000000000..70a85e81df85 --- /dev/null +++ b/tests/neg/abstract-tracked.check @@ -0,0 +1,20 @@ +-- [E156] Syntax Error: tests/neg/abstract-tracked.scala:4:14 ---------------------------------------------------------- +4 |tracked trait F // error + |^^^^^^^^^^^^^^^ + |Modifier tracked is not allowed for this definition +-- [E156] Syntax Error: tests/neg/abstract-tracked.scala:9:15 ---------------------------------------------------------- +9 |tracked object O // error + |^^^^^^^^^^^^^^^^ + |Modifier tracked is not allowed for this definition +-- [E156] Syntax Error: tests/neg/abstract-tracked.scala:11:14 --------------------------------------------------------- +11 |tracked class C // error + |^^^^^^^^^^^^^^^ + |Modifier tracked is not allowed for this definition +-- [E156] Syntax Error: tests/neg/abstract-tracked.scala:7:14 ---------------------------------------------------------- +7 | tracked def f: F // error + | ^^^^^^^^^^^^^^^^ + | Modifier tracked is not allowed for this definition +-- [E156] Syntax Error: tests/neg/abstract-tracked.scala:14:14 --------------------------------------------------------- +14 | tracked val x = 1 // error + | ^^^^^^^^^^^^^^^^^ + | Modifier tracked is not allowed for this definition diff --git a/tests/neg/abstract-tracked.scala b/tests/neg/abstract-tracked.scala new file mode 100644 index 000000000000..ff4a7ea8174f --- /dev/null +++ b/tests/neg/abstract-tracked.scala @@ -0,0 +1,14 @@ +import scala.language.experimental.modularity +import scala.language.future + +tracked trait F // error + +trait G: + tracked def f: F // error + +tracked object O // error + +tracked class C // error + +def f = + tracked val x = 1 // error diff --git a/tests/neg/tracked.check b/tests/neg/tracked.check index 14a4d2a08300..3494c401a007 100644 --- a/tests/neg/tracked.check +++ b/tests/neg/tracked.check @@ -6,22 +6,6 @@ 7 | def foo(tracked a: Int) = // error | ^ | ':' expected, but identifier found --- Error: tests/neg/tracked.scala:8:12 --------------------------------------------------------------------------------- -8 | tracked val b: Int = 2 // error - | ^^^ - | end of statement expected but 'val' found --- Error: tests/neg/tracked.scala:11:10 -------------------------------------------------------------------------------- -11 | tracked object Foo // error // error - | ^^^^^^ - | end of statement expected but 'object' found --- Error: tests/neg/tracked.scala:14:10 -------------------------------------------------------------------------------- -14 | tracked class D // error // error - | ^^^^^ - | end of statement expected but 'class' found --- Error: tests/neg/tracked.scala:17:10 -------------------------------------------------------------------------------- -17 | tracked type T = Int // error // error - | ^^^^ - | end of statement expected but 'type' found -- Error: tests/neg/tracked.scala:20:25 -------------------------------------------------------------------------------- 20 | given g2: (tracked val x: Int) => C = C(x) // error | ^^^^^^^^^^^^^^^^^^ @@ -30,21 +14,19 @@ 4 |class C2(tracked var x: Int) // error | ^ | mutable variables may not be `tracked` --- [E006] Not Found Error: tests/neg/tracked.scala:11:2 ---------------------------------------------------------------- -11 | tracked object Foo // error // error - | ^^^^^^^ - | Not found: tracked - | - | longer explanation available when compiling with `-explain` --- [E006] Not Found Error: tests/neg/tracked.scala:14:2 ---------------------------------------------------------------- -14 | tracked class D // error // error - | ^^^^^^^ - | Not found: tracked - | - | longer explanation available when compiling with `-explain` --- [E006] Not Found Error: tests/neg/tracked.scala:17:2 ---------------------------------------------------------------- -17 | tracked type T = Int // error // error - | ^^^^^^^ - | Not found: tracked - | - | longer explanation available when compiling with `-explain` +-- [E156] Syntax Error: tests/neg/tracked.scala:8:16 ------------------------------------------------------------------- +8 | tracked val b: Int = 2 // error + | ^^^^^^^^^^^^^^^^^^^^^^ + | Modifier tracked is not allowed for this definition +-- [E156] Syntax Error: tests/neg/tracked.scala:11:17 ------------------------------------------------------------------ +11 | tracked object Foo // error + | ^^^^^^^^^^^^^^^^^^ + | Modifier tracked is not allowed for this definition +-- [E156] Syntax Error: tests/neg/tracked.scala:14:16 ------------------------------------------------------------------ +14 | tracked class D // error + | ^^^^^^^^^^^^^^^ + | Modifier tracked is not allowed for this definition +-- [E156] Syntax Error: tests/neg/tracked.scala:17:15 ------------------------------------------------------------------ +17 | tracked type T = Int // error + | ^^^^^^^^^^^^^^^^^^^^ + | Modifier tracked is not allowed for this definition diff --git a/tests/neg/tracked.scala b/tests/neg/tracked.scala index 9f874ca3c0da..3d6c1a14fc55 100644 --- a/tests/neg/tracked.scala +++ b/tests/neg/tracked.scala @@ -8,13 +8,13 @@ object A: tracked val b: Int = 2 // error object B: - tracked object Foo // error // error + tracked object Foo // error object C: - tracked class D // error // error + tracked class D // error object D: - tracked type T = Int // error // error + tracked type T = Int // error object E: given g2: (tracked val x: Int) => C = C(x) // error diff --git a/tests/pos/abstract-tracked-2.scala b/tests/pos/abstract-tracked-2.scala new file mode 100644 index 000000000000..01e4ee84c548 --- /dev/null +++ b/tests/pos/abstract-tracked-2.scala @@ -0,0 +1,11 @@ +import scala.language.experimental.modularity +import scala.language.future + +abstract class Vec: + tracked val size: Int + +@main def main = + val v = new Vec: + val size0: size.type = 10 + val size = 10 + val size1: size.type = 10 diff --git a/tests/pos/abstract-tracked.scala b/tests/pos/abstract-tracked.scala new file mode 100644 index 000000000000..21812db9c04d --- /dev/null +++ b/tests/pos/abstract-tracked.scala @@ -0,0 +1,55 @@ +import scala.language.experimental.modularity +import scala.language.future + +trait F: + tracked val a: Int + +trait G: + tracked val b: Int + +trait H: + tracked val c: Int = 3 + +trait I extends F + +trait J extends F: + val a: Int = 1 + +class K(tracked val d: Int) + +class L + +trait M: + val f: Int + +class N extends F: + val a = 10 + +object Test: + val f = new F: + val a = 1 + val g = new G: + val b: 2 = 2 + val h = new H: + override val c = 4 + val i = new I: + val a = 5 + val j = new J: + override val a = 6 + val k = new K(7) + val l = new L { + tracked val e = 8 + } + val m = new M: + tracked val f = 9 + val n = new N + + summon[f.a.type <:< 1] + summon[g.b.type <:< 2] + summon[h.c.type <:< 4] + summon[i.a.type <:< 5] + summon[j.a.type <:< 6] + summon[k.d.type <:< 7] + // summon[l.e.type <:< 8] // unrelated issue -- error: e is not a member of L + summon[m.f.type <:< 9] + summon[n.a.type <:< 10]