Skip to content
3 changes: 0 additions & 3 deletions compiler/src/dotty/tools/dotc/core/SymDenotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
105 changes: 39 additions & 66 deletions compiler/src/dotty/tools/dotc/typer/Namer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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)

Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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!
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 =>
Expand All @@ -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 _ =>
Expand Down
39 changes: 18 additions & 21 deletions docs/_docs/reference/experimental/modularity.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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

Expand Down
6 changes: 6 additions & 0 deletions tests/pos/tracked.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import scala.language.experimental.modularity
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be good to add some more tests that exercise the precise conditions that infer tracked.


trait T:
type X

class C(var t: T)
Loading