Skip to content

Commit 7057332

Browse files
authored
Simpler inference for tracked (#22972)
Change `tracked` inference to simpler heuristic: always infer `tracked` for a parameter if its type has an abstract type member. Tested it with `modularity` turned on and the only bug I noticed was eager typing of annotations, which I patched by not inferring `tracked` for annotated parameters.
2 parents 60bd4b5 + 069dd61 commit 7057332

File tree

4 files changed

+63
-90
lines changed

4 files changed

+63
-90
lines changed

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2801,9 +2801,6 @@ object SymDenotations {
28012801
/** Sets all missing fields of given denotation */
28022802
def complete(denot: SymDenotation)(using Context): Unit
28032803

2804-
/** Is this a completer for an explicit type tree */
2805-
def isExplicit: Boolean = false
2806-
28072804
def apply(sym: Symbol): LazyType = this
28082805
def apply(module: TermSymbol, modcls: ClassSymbol): LazyType = this
28092806

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

Lines changed: 39 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -280,9 +280,6 @@ class Namer { typer: Typer =>
280280
if rhs.isEmpty || flags.is(Opaque) then flags |= Deferred
281281
if flags.is(Param) then tree.rhs else analyzeRHS(tree.rhs)
282282

283-
def hasExplicitType(tree: ValOrDefDef): Boolean =
284-
!tree.tpt.isEmpty || tree.mods.isOneOf(TermParamOrAccessor)
285-
286283
// to complete a constructor, move one context further out -- this
287284
// is the context enclosing the class. Note that the context in which a
288285
// constructor is recorded and the context in which it is completed are
@@ -296,8 +293,6 @@ class Namer { typer: Typer =>
296293

297294
val completer = tree match
298295
case tree: TypeDef => TypeDefCompleter(tree)(cctx)
299-
case tree: ValOrDefDef if Feature.enabled(Feature.modularity) && hasExplicitType(tree) =>
300-
new Completer(tree, isExplicit = true)(cctx)
301296
case _ => Completer(tree)(cctx)
302297
val info = adjustIfModule(completer, tree)
303298
createOrRefine[Symbol](tree, name, flags, ctx.owner, _ => info,
@@ -813,7 +808,7 @@ class Namer { typer: Typer =>
813808
}
814809

815810
/** The completer of a symbol defined by a member def or import (except ClassSymbols) */
816-
class Completer(val original: Tree, override val isExplicit: Boolean = false)(ictx: Context) extends LazyType with SymbolLoaders.SecondCompleter {
811+
class Completer(val original: Tree)(ictx: Context) extends LazyType with SymbolLoaders.SecondCompleter {
817812

818813
protected def localContext(owner: Symbol): FreshContext = ctx.fresh.setOwner(owner).setTree(original)
819814

@@ -830,6 +825,11 @@ class Namer { typer: Typer =>
830825
def setNotNullInfos(infos: List[NotNullInfo]): Unit =
831826
myNotNullInfos = infos
832827

828+
/** Cache for type signature if computed without forcing annotations
829+
* by `typeSigOnly`
830+
*/
831+
private var knownTypeSig: Type = NoType
832+
833833
protected def typeSig(sym: Symbol): Type = original match
834834
case original: ValDef =>
835835
if (sym.is(Module)) moduleValSig(sym)
@@ -1006,12 +1006,20 @@ class Namer { typer: Typer =>
10061006
val sym = denot.symbol
10071007
addAnnotations(sym)
10081008
addInlineInfo(sym)
1009-
denot.info = typeSig(sym)
1009+
denot.info = knownTypeSig `orElse` typeSig(sym)
10101010
invalidateIfClashingSynthetic(denot)
10111011
normalizeFlags(denot)
10121012
Checking.checkWellFormed(sym)
10131013
denot.info = avoidPrivateLeaks(sym)
10141014
}
1015+
1016+
/** Just the type signature without forcing any of the other parts of
1017+
* this denotation. The denotation will still be completed later.
1018+
*/
1019+
def typeSigOnly(sym: Symbol): Type =
1020+
if !knownTypeSig.exists then
1021+
knownTypeSig = typeSig(sym)
1022+
knownTypeSig
10151023
}
10161024

10171025
class TypeDefCompleter(original: TypeDef)(ictx: Context)
@@ -1925,6 +1933,10 @@ class Namer { typer: Typer =>
19251933
else mbrTpe
19261934
}
19271935

1936+
// Decides whether we want to run tracked inference on all code, not just
1937+
// code with x.modularity
1938+
private inline val testTrackedInference = false
1939+
19281940
/** The type signature of a DefDef with given symbol */
19291941
def defDefSig(ddef: DefDef, sym: Symbol, completer: Namer#Completer)(using Context): Type =
19301942
// Beware: ddef.name need not match sym.name if sym was freshened!
@@ -1973,11 +1985,12 @@ class Namer { typer: Typer =>
19731985
def addTrackedIfNeeded(ddef: DefDef, owningSym: Symbol): Unit =
19741986
for params <- ddef.termParamss; param <- params do
19751987
val psym = symbolOfTree(param)
1976-
if needsTracked(psym, param, owningSym) then
1988+
if needsTracked(psym, param, owningSym) && Feature.enabled(modularity) then
19771989
psym.setFlag(Tracked)
19781990
setParamTrackedWithAccessors(psym, sym.maybeOwner.infoOrCompleter)
19791991

1980-
if Feature.enabled(modularity) then addTrackedIfNeeded(ddef, sym.maybeOwner)
1992+
if Feature.enabled(modularity) || testTrackedInference then
1993+
addTrackedIfNeeded(ddef, sym.maybeOwner)
19811994

19821995
if isConstructor then
19831996
// 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 =>
20302043
psym.setFlag(Tracked)
20312044
acc.setFlag(Tracked)
20322045

2033-
/** `psym` needs tracked if it is referenced in any of the public signatures
2034-
* of the defining class or when `psym` is a context bound witness with an
2035-
* abstract type member
2046+
/** `psym` needs an inferred tracked if
2047+
* - it is a val parameter of a class or
2048+
* an evidence parameter of a context bound witness, and
2049+
* - its type contains an abstract type member.
20362050
*/
20372051
def needsTracked(psym: Symbol, param: ValDef, owningSym: Symbol)(using Context) =
2038-
lazy val abstractContextBound = isContextBoundWitnessWithAbstractMembers(psym, param, owningSym)
2039-
lazy val isRefInSignatures =
2040-
psym.maybeOwner.isPrimaryConstructor
2041-
&& isReferencedInPublicSignatures(psym)
2052+
lazy val accessorSyms = maybeParamAccessors(owningSym, psym)
2053+
2054+
def infoDontForceAnnots = psym.infoOrCompleter match
2055+
case completer: this.Completer => completer.typeSigOnly(psym)
2056+
case tpe => tpe
2057+
20422058
!psym.is(Tracked)
2043-
&& psym.isTerm
2044-
&& (
2045-
abstractContextBound
2046-
|| isRefInSignatures
2047-
)
2048-
2049-
/** Under x.modularity, we add `tracked` to context bound witnesses and
2050-
* explicit evidence parameters that have abstract type members
2051-
*/
2052-
private def isContextBoundWitnessWithAbstractMembers(psym: Symbol, param: ValDef, owningSym: Symbol)(using Context): Boolean =
2053-
val accessorSyms = maybeParamAccessors(owningSym, psym)
2054-
(owningSym.isClass || owningSym.isAllOf(Given | Method))
2055-
&& (param.hasAttachment(ContextBoundParam) || (psym.isOneOf(GivenOrImplicit) && !accessorSyms.forall(_.isOneOf(PrivateLocal))))
2056-
&& psym.info.memberNames(abstractTypeNameFilter).nonEmpty
2057-
2058-
extension (sym: Symbol)
2059-
private def infoWithForceNonInferingCompleter(using Context): Type = sym.infoOrCompleter match
2060-
case tpe: LazyType if tpe.isExplicit => sym.info
2061-
case tpe if sym.isType => sym.info
2062-
case info => info
2063-
2064-
/** Under x.modularity, we add `tracked` to term parameters whose types are
2065-
* referenced in public signatures of the defining class
2066-
*/
2067-
private def isReferencedInPublicSignatures(sym: Symbol)(using Context): Boolean =
2068-
val owner = sym.maybeOwner.maybeOwner
2069-
val accessorSyms = maybeParamAccessors(owner, sym)
2070-
def checkOwnerMemberSignatures(owner: Symbol): Boolean =
2071-
owner.infoOrCompleter match
2072-
case info: ClassInfo =>
2073-
info.decls.filter(_.isPublic)
2074-
.filter(_ != sym.maybeOwner)
2075-
.exists { decl =>
2076-
tpeContainsSymbolRef(decl.infoWithForceNonInferingCompleter, accessorSyms)
2077-
}
2078-
case _ => false
2079-
checkOwnerMemberSignatures(owner)
2080-
2081-
/** Check if any of syms are referenced in tpe */
2082-
private def tpeContainsSymbolRef(tpe: Type, syms: List[Symbol])(using Context): Boolean =
2083-
val acc = new ExistsAccumulator(
2084-
{ tpe => tpe.termSymbol.exists && syms.contains(tpe.termSymbol) },
2085-
StopAt.Static,
2086-
forceLazy = false
2087-
) {
2088-
override def apply(acc: Boolean, tpe: Type): Boolean = super.apply(acc, tpe.safeDealias)
2089-
}
2090-
acc(false, tpe)
2059+
&& psym.isTerm
2060+
&& (owningSym.isClass || owningSym.isAllOf(Given | Method))
2061+
&& accessorSyms.forall(!_.is(Mutable))
2062+
&& (param.hasAttachment(ContextBoundParam) || accessorSyms.exists(!_.isOneOf(PrivateLocal)))
2063+
&& infoDontForceAnnots.abstractTypeMembers.nonEmpty
2064+
end needsTracked
20912065

20922066
private def maybeParamAccessors(owner: Symbol, sym: Symbol)(using Context): List[Symbol] = owner.infoOrCompleter match
20932067
case info: ClassInfo =>
@@ -2102,8 +2076,7 @@ class Namer { typer: Typer =>
21022076
def setTrackedConstrParam(param: ValDef)(using Context): Unit =
21032077
val sym = symbolOfTree(param)
21042078
sym.maybeOwner.maybeOwner.infoOrCompleter match
2105-
case info: ClassInfo
2106-
if !sym.is(Tracked) && isContextBoundWitnessWithAbstractMembers(sym, param, sym.maybeOwner.maybeOwner) =>
2079+
case info: ClassInfo if needsTracked(sym, param, sym.maybeOwner.maybeOwner) =>
21072080
typr.println(i"set tracked $param, $sym: ${sym.info} containing ${sym.info.memberNames(abstractTypeNameFilter).toList}")
21082081
setParamTrackedWithAccessors(sym, info)
21092082
case _ =>

docs/_docs/reference/experimental/modularity.md

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -116,25 +116,22 @@ ClsParam ::= {Annotation} [{Modifier | ‘tracked’} (‘val’ | ‘var’)]
116116

117117
The (soft) `tracked` modifier is only allowed for `val` parameters of classes.
118118

119-
### Tracked inference
119+
### Tracked Inference
120120

121-
In some cases `tracked` can be infered and doesn't have to be written
122-
explicitly. A common such case is when a class parameter is referenced in the
123-
signatures of the public members of the class. e.g.
124-
```scala 3
125-
class OrdSet(val ord: Ordering) {
126-
type Set = List[ord.T]
127-
def empty: Set = Nil
121+
In some common cases the tracked modifier can be inferred, so it does not
122+
need to be written explicitly. Specifically, we infer `tracked` for a `val`
123+
parameter of a class if the formal parameter's type defines an abstract type member.
124+
This means that we do not lose information about how that member
125+
is defined in the actual argument passed to the class constructor.
128126

129-
implicit class helper(s: Set) {
130-
def add(x: ord.T): Set = x :: remove(x)
131-
def remove(x: ord.T): Set = s.filter(e => ord.compare(x, e) != 0)
132-
def member(x: ord.T): Boolean = s.exists(e => ord.compare(x, e) == 0)
133-
}
134-
}
127+
For instance, tracked `would` be inferred for the `SetFunctor` class
128+
we defined before, so we can also write it like this:
129+
```scala
130+
class SetFunctor(val ord: Ordering):
131+
type Set = List[ord.T]
132+
...
135133
```
136-
In the example above, `ord` is referenced in the signatures of the public
137-
members of `OrdSet`, so a `tracked` modifier will be inserted automatically.
134+
The `tracked` modifier on the `ord` parameter is inferred here, since `ord` is of type `Ordering`, which defines an abstract type member `T`.
138135

139136
Another common case is when a context bound has an associated type (i.e. an abstract type member) e.g.
140137
```scala 3
@@ -145,7 +142,7 @@ trait TC:
145142
class Klass[A: {TC as tc}]
146143
```
147144

148-
Here, `tc` is a context bound with an associated type `T`, so `tracked` will be inferred for `tc`.
145+
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.
149146

150147
### Discussion
151148

@@ -160,10 +157,10 @@ If we assume `tracked` for parameter `x` (which is implicitly a `val`),
160157
then `foo` would get inferred type `Foo { val x: 1 }`, so it could not
161158
be reassigned to a value of type `Foo { val x: 2 }` on the next line.
162159

163-
Another approach might be to assume `tracked` for a `val` parameter `x`
164-
only if the class refers to a type member of `x`. But it turns out that this
165-
scheme is unimplementable since it would quickly lead to cyclic references
166-
when typechecking recursive class graphs. So an explicit `tracked` looks like the best available option.
160+
Another concern is that using tracked for all `val` parameters, including
161+
parameters of case classes could lead to large refinement types.
162+
163+
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.
167164

168165
## Tracked members
169166

tests/pos/tracked.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import scala.language.experimental.modularity
2+
3+
trait T:
4+
type X
5+
6+
class C(var t: T)

0 commit comments

Comments
 (0)