diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 6dcf2a83ba5c..5b7127d1cdf9 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -2799,9 +2799,6 @@ object SymDenotations { /** Sets all missing fields of given denotation */ def complete(denot: SymDenotation)(using Context): Unit - /** Is this a completer for an explicit type tree */ - def isExplicit: Boolean = false - def apply(sym: Symbol): LazyType = this def apply(module: TermSymbol, modcls: ClassSymbol): LazyType = this diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 65c9fa2f5ddc..02a55be9ea5a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -280,9 +280,6 @@ class Namer { typer: Typer => if rhs.isEmpty || flags.is(Opaque) then flags |= Deferred if flags.is(Param) then tree.rhs else analyzeRHS(tree.rhs) - def hasExplicitType(tree: ValOrDefDef): Boolean = - !tree.tpt.isEmpty || tree.mods.isOneOf(TermParamOrAccessor) - // to complete a constructor, move one context further out -- this // is the context enclosing the class. Note that the context in which a // constructor is recorded and the context in which it is completed are @@ -296,8 +293,6 @@ class Namer { typer: Typer => val completer = tree match case tree: TypeDef => TypeDefCompleter(tree)(cctx) - case tree: ValOrDefDef if Feature.enabled(Feature.modularity) && hasExplicitType(tree) => - new Completer(tree, isExplicit = true)(cctx) case _ => Completer(tree)(cctx) val info = adjustIfModule(completer, tree) createOrRefine[Symbol](tree, name, flags, ctx.owner, _ => info, @@ -813,7 +808,7 @@ class Namer { typer: Typer => } /** The completer of a symbol defined by a member def or import (except ClassSymbols) */ - class Completer(val original: Tree, override val isExplicit: Boolean = false)(ictx: Context) extends LazyType with SymbolLoaders.SecondCompleter { + class Completer(val original: Tree)(ictx: Context) extends LazyType with SymbolLoaders.SecondCompleter { protected def localContext(owner: Symbol): FreshContext = ctx.fresh.setOwner(owner).setTree(original) @@ -830,6 +825,11 @@ class Namer { typer: Typer => def setNotNullInfos(infos: List[NotNullInfo]): Unit = myNotNullInfos = infos + /** Cache for type signature if computed without forcing annotations + * by `typeSigOnly` + */ + private var knownTypeSig: Type = NoType + protected def typeSig(sym: Symbol): Type = original match case original: ValDef => if (sym.is(Module)) moduleValSig(sym) @@ -1006,12 +1006,20 @@ class Namer { typer: Typer => val sym = denot.symbol addAnnotations(sym) addInlineInfo(sym) - denot.info = typeSig(sym) + denot.info = knownTypeSig `orElse` typeSig(sym) invalidateIfClashingSynthetic(denot) normalizeFlags(denot) Checking.checkWellFormed(sym) denot.info = avoidPrivateLeaks(sym) } + + /** Just the type signature without forcing any of the other parts of + * this denotation. The denotation will still be completed later. + */ + def typeSigOnly(sym: Symbol): Type = + if !knownTypeSig.exists then + knownTypeSig = typeSig(sym) + knownTypeSig } class TypeDefCompleter(original: TypeDef)(ictx: Context) @@ -1925,6 +1933,10 @@ class Namer { typer: Typer => else mbrTpe } + // Decides whether we want to run tracked inference on all code, not just + // code with x.modularity + private inline val testTrackedInference = false + /** The type signature of a DefDef with given symbol */ def defDefSig(ddef: DefDef, sym: Symbol, completer: Namer#Completer)(using Context): Type = // Beware: ddef.name need not match sym.name if sym was freshened! @@ -1973,11 +1985,12 @@ class Namer { typer: Typer => def addTrackedIfNeeded(ddef: DefDef, owningSym: Symbol): Unit = for params <- ddef.termParamss; param <- params do val psym = symbolOfTree(param) - if needsTracked(psym, param, owningSym) then + if needsTracked(psym, param, owningSym) && Feature.enabled(modularity) then psym.setFlag(Tracked) setParamTrackedWithAccessors(psym, sym.maybeOwner.infoOrCompleter) - if Feature.enabled(modularity) then addTrackedIfNeeded(ddef, sym.maybeOwner) + if Feature.enabled(modularity) || testTrackedInference then + addTrackedIfNeeded(ddef, sym.maybeOwner) if isConstructor then // set result type tree to unit, but take the current class as result type of the symbol @@ -2030,64 +2043,25 @@ class Namer { typer: Typer => psym.setFlag(Tracked) acc.setFlag(Tracked) - /** `psym` needs tracked if it is referenced in any of the public signatures - * of the defining class or when `psym` is a context bound witness with an - * abstract type member + /** `psym` needs an inferred tracked if + * - it is a val parameter of a class or + * an evidence parameter of a context bound witness, and + * - its type contains an abstract type member. */ def needsTracked(psym: Symbol, param: ValDef, owningSym: Symbol)(using Context) = - lazy val abstractContextBound = isContextBoundWitnessWithAbstractMembers(psym, param, owningSym) - lazy val isRefInSignatures = - psym.maybeOwner.isPrimaryConstructor - && isReferencedInPublicSignatures(psym) + lazy val accessorSyms = maybeParamAccessors(owningSym, psym) + + def infoDontForceAnnots = psym.infoOrCompleter match + case completer: this.Completer => completer.typeSigOnly(psym) + case tpe => tpe + !psym.is(Tracked) - && psym.isTerm - && ( - abstractContextBound - || isRefInSignatures - ) - - /** Under x.modularity, we add `tracked` to context bound witnesses and - * explicit evidence parameters that have abstract type members - */ - private def isContextBoundWitnessWithAbstractMembers(psym: Symbol, param: ValDef, owningSym: Symbol)(using Context): Boolean = - val accessorSyms = maybeParamAccessors(owningSym, psym) - (owningSym.isClass || owningSym.isAllOf(Given | Method)) - && (param.hasAttachment(ContextBoundParam) || (psym.isOneOf(GivenOrImplicit) && !accessorSyms.forall(_.isOneOf(PrivateLocal)))) - && psym.info.memberNames(abstractTypeNameFilter).nonEmpty - - extension (sym: Symbol) - private def infoWithForceNonInferingCompleter(using Context): Type = sym.infoOrCompleter match - case tpe: LazyType if tpe.isExplicit => sym.info - case tpe if sym.isType => sym.info - case info => info - - /** Under x.modularity, we add `tracked` to term parameters whose types are - * referenced in public signatures of the defining class - */ - private def isReferencedInPublicSignatures(sym: Symbol)(using Context): Boolean = - val owner = sym.maybeOwner.maybeOwner - val accessorSyms = maybeParamAccessors(owner, sym) - def checkOwnerMemberSignatures(owner: Symbol): Boolean = - owner.infoOrCompleter match - case info: ClassInfo => - info.decls.filter(_.isPublic) - .filter(_ != sym.maybeOwner) - .exists { decl => - tpeContainsSymbolRef(decl.infoWithForceNonInferingCompleter, accessorSyms) - } - case _ => false - checkOwnerMemberSignatures(owner) - - /** Check if any of syms are referenced in tpe */ - private def tpeContainsSymbolRef(tpe: Type, syms: List[Symbol])(using Context): Boolean = - val acc = new ExistsAccumulator( - { tpe => tpe.termSymbol.exists && syms.contains(tpe.termSymbol) }, - StopAt.Static, - forceLazy = false - ) { - override def apply(acc: Boolean, tpe: Type): Boolean = super.apply(acc, tpe.safeDealias) - } - acc(false, tpe) + && psym.isTerm + && (owningSym.isClass || owningSym.isAllOf(Given | Method)) + && accessorSyms.forall(!_.is(Mutable)) + && (param.hasAttachment(ContextBoundParam) || accessorSyms.exists(!_.isOneOf(PrivateLocal))) + && infoDontForceAnnots.abstractTypeMembers.nonEmpty + end needsTracked private def maybeParamAccessors(owner: Symbol, sym: Symbol)(using Context): List[Symbol] = owner.infoOrCompleter match case info: ClassInfo => @@ -2102,8 +2076,7 @@ class Namer { typer: Typer => def setTrackedConstrParam(param: ValDef)(using Context): Unit = val sym = symbolOfTree(param) sym.maybeOwner.maybeOwner.infoOrCompleter match - case info: ClassInfo - if !sym.is(Tracked) && isContextBoundWitnessWithAbstractMembers(sym, param, sym.maybeOwner.maybeOwner) => + case info: ClassInfo if needsTracked(sym, param, sym.maybeOwner.maybeOwner) => typr.println(i"set tracked $param, $sym: ${sym.info} containing ${sym.info.memberNames(abstractTypeNameFilter).toList}") setParamTrackedWithAccessors(sym, info) case _ => diff --git a/docs/_docs/reference/experimental/modularity.md b/docs/_docs/reference/experimental/modularity.md index 580044ce4d66..82481dfefad1 100644 --- a/docs/_docs/reference/experimental/modularity.md +++ b/docs/_docs/reference/experimental/modularity.md @@ -116,25 +116,22 @@ ClsParam ::= {Annotation} [{Modifier | ‘tracked’} (‘val’ | ‘var’)] The (soft) `tracked` modifier is only allowed for `val` parameters of classes. -### Tracked inference +### Tracked Inference -In some cases `tracked` can be infered and doesn't have to be written -explicitly. A common such case is when a class parameter is referenced in the -signatures of the public members of the class. e.g. -```scala 3 -class OrdSet(val ord: Ordering) { - type Set = List[ord.T] - def empty: Set = Nil +In some common cases the tracked modifier can be inferred, so it does not +need to be written explicitly. Specifically, we infer `tracked` for a `val` +parameter of a class if the formal parameter's type defines an abstract type member. +This means that we do not lose information about how that member +is defined in the actual argument passed to the class constructor. - implicit class helper(s: Set) { - def add(x: ord.T): Set = x :: remove(x) - def remove(x: ord.T): Set = s.filter(e => ord.compare(x, e) != 0) - def member(x: ord.T): Boolean = s.exists(e => ord.compare(x, e) == 0) - } -} +For instance, tracked `would` be inferred for the `SetFunctor` class +we defined before, so we can also write it like this: +```scala +class SetFunctor(val ord: Ordering): + type Set = List[ord.T] + ... ``` -In the example above, `ord` is referenced in the signatures of the public -members of `OrdSet`, so a `tracked` modifier will be inserted automatically. +The `tracked` modifier on the `ord` parameter is inferred here, since `ord` is of type `Ordering`, which defines an abstract type member `T`. Another common case is when a context bound has an associated type (i.e. an abstract type member) e.g. ```scala 3 @@ -145,7 +142,7 @@ trait TC: class Klass[A: {TC as tc}] ``` -Here, `tc` is a context bound with an associated type `T`, so `tracked` will be inferred for `tc`. +Here, `tc` is a context bound with an associated type `T`, so `tracked val` will be inferred for `tc` and the parameter will be represented as a field. ### Discussion @@ -160,10 +157,10 @@ If we assume `tracked` for parameter `x` (which is implicitly a `val`), then `foo` would get inferred type `Foo { val x: 1 }`, so it could not be reassigned to a value of type `Foo { val x: 2 }` on the next line. -Another approach might be to assume `tracked` for a `val` parameter `x` -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. +Another concern is that using tracked for all `val` parameters, including +parameters of case classes could lead to large refinement types. + +Therefore, inferring tracked only for parameters with types that define abstract members is a usable compromise. After all, if we did not infer `tracked` for these types, any references to the abstract type via a path would likely produce compilation errors. ## Tracked members diff --git a/tests/pos/tracked.scala b/tests/pos/tracked.scala new file mode 100644 index 000000000000..0933593a98d8 --- /dev/null +++ b/tests/pos/tracked.scala @@ -0,0 +1,6 @@ +import scala.language.experimental.modularity + +trait T: + type X + +class C(var t: T)