Skip to content

Commit 0041987

Browse files
KacperFKorbanmbovelodersky
authored
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 <[email protected]> Co-authored-by: odersky <[email protected]>
1 parent 4b7f321 commit 0041987

File tree

15 files changed

+198
-62
lines changed

15 files changed

+198
-62
lines changed

Diff for: compiler/src/dotty/tools/dotc/ast/Desugar.scala

+10-2
Original file line numberDiff line numberDiff line change
@@ -1086,12 +1086,13 @@ object desugar {
10861086
if mods.isAllOf(Given | Inline | Transparent) then
10871087
report.error("inline given instances cannot be trasparent", cdef)
10881088
var classMods = if mods.is(Given) then mods &~ (Inline | Transparent) | Synthetic else mods
1089-
if vparamAccessors.exists(_.mods.is(Tracked)) then
1089+
val newBody = tparamAccessors ::: vparamAccessors ::: normalizedBody ::: caseClassMeths
1090+
if newBody.collect { case d: ValOrDefDef => d }.exists(_.mods.is(Tracked)) then
10901091
classMods |= Dependent
10911092
cpy.TypeDef(cdef: TypeDef)(
10921093
name = className,
10931094
rhs = cpy.Template(impl)(constr, parents1, clsDerived, self1,
1094-
tparamAccessors ::: vparamAccessors ::: normalizedBody ::: caseClassMeths)
1095+
newBody)
10951096
).withMods(classMods)
10961097
}
10971098

@@ -1561,6 +1562,12 @@ object desugar {
15611562
rhsOK(rhs)
15621563
}
15631564

1565+
val legalTracked: Context ?=> MemberDefTest = {
1566+
case valdef @ ValDef(_, _, _) =>
1567+
val sym = valdef.symbol
1568+
!ctx.owner.exists || ctx.owner.isClass || ctx.owner.is(Case) || ctx.owner.isConstructor || valdef.mods.is(Param) || valdef.mods.is(ParamAccessor)
1569+
}
1570+
15641571
def checkOpaqueAlias(tree: MemberDef)(using Context): MemberDef =
15651572
def check(rhs: Tree): MemberDef = rhs match
15661573
case bounds: TypeBoundsTree if bounds.alias.isEmpty =>
@@ -1586,6 +1593,7 @@ object desugar {
15861593
} else tested
15871594
tested = checkOpaqueAlias(tested)
15881595
tested = checkApplicable(Opaque, legalOpaque)
1596+
tested = checkApplicable(Tracked, legalTracked)
15891597
tested
15901598
case _ =>
15911599
tree

Diff for: compiler/src/dotty/tools/dotc/core/Flags.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ object Flags {
480480
*/
481481
val AfterLoadFlags: FlagSet = commonFlags(
482482
FromStartFlags, AccessFlags, Final, AccessorOrSealed,
483-
Abstract, LazyOrTrait, SelfName, JavaDefined, JavaAnnotation, Transparent, Tracked)
483+
Abstract, LazyOrTrait, SelfName, JavaDefined, JavaAnnotation, Transparent)
484484

485485
/** A value that's unstable unless complemented with a Stable flag */
486486
val UnstableValueFlags: FlagSet = Mutable | Method

Diff for: compiler/src/dotty/tools/dotc/parsing/Parsers.scala

+1-3
Original file line numberDiff line numberDiff line change
@@ -3519,7 +3519,7 @@ object Parsers {
35193519
* UsingClsTermParamClause::= ‘(’ ‘using’ [‘erased’] (ClsParams | ContextTypes) ‘)’
35203520
* ClsParams ::= ClsParam {‘,’ ClsParam}
35213521
* ClsParam ::= {Annotation}
3522-
* [{Modifier | ‘tracked’} (‘val’ | ‘var’)] Param
3522+
* [{Modifier} (‘val’ | ‘var’)] Param
35233523
* TypelessClause ::= DefTermParamClause
35243524
* | UsingParamClause
35253525
*
@@ -3557,8 +3557,6 @@ object Parsers {
35573557
if isErasedKw then
35583558
mods = addModifier(mods)
35593559
if paramOwner.isClass then
3560-
if isIdent(nme.tracked) && in.featureEnabled(Feature.modularity) && !in.lookahead.isColon then
3561-
mods = addModifier(mods)
35623560
mods = addFlag(modifiers(start = mods), ParamAccessor)
35633561
mods =
35643562
if in.token == VAL then

Diff for: compiler/src/dotty/tools/dotc/parsing/Scanners.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ object Scanners {
212212

213213
def featureEnabled(name: TermName) = Feature.enabled(name)(using languageImportContext)
214214
def erasedEnabled = featureEnabled(Feature.erasedDefinitions)
215+
def trackedEnabled = featureEnabled(Feature.modularity)
215216

216217
private var postfixOpsEnabledCache = false
217218
private var postfixOpsEnabledCtx: Context = NoContext
@@ -1195,7 +1196,7 @@ object Scanners {
11951196

11961197
def isSoftModifier: Boolean =
11971198
token == IDENTIFIER
1198-
&& (softModifierNames.contains(name) || name == nme.erased && erasedEnabled)
1199+
&& (softModifierNames.contains(name) || name == nme.erased && erasedEnabled || name == nme.tracked && trackedEnabled)
11991200

12001201
def isSoftModifierInModifierPosition: Boolean =
12011202
isSoftModifier && inModifierPosition()

Diff for: compiler/src/dotty/tools/dotc/typer/Namer.scala

+12-4
Original file line numberDiff line numberDiff line change
@@ -878,7 +878,7 @@ class Namer { typer: Typer =>
878878
case original: untpd.MemberDef =>
879879
lazy val annotCtx = annotContext(original, sym)
880880
original.setMods:
881-
original.mods.withAnnotations :
881+
original.mods.withAnnotations:
882882
original.mods.annotations.mapConserve: annotTree =>
883883
val cls = typedAheadAnnotationClass(annotTree)(using annotCtx)
884884
if (cls eq sym)
@@ -2017,6 +2017,11 @@ class Namer { typer: Typer =>
20172017
paramFn: Type => Type,
20182018
fallbackProto: Type
20192019
)(using Context): Type =
2020+
/** Is this member tracked? This is true if it is marked as `tracked` or if
2021+
* it overrides a `tracked` member. To account for the later, `isTracked`
2022+
* is overriden to `true` as a side-effect of computing `inherited`.
2023+
*/
2024+
var isTracked: Boolean = sym.is(Tracked)
20202025

20212026
/** A type for this definition that might be inherited from elsewhere:
20222027
* If this is a setter parameter, the corresponding getter type.
@@ -2052,8 +2057,10 @@ class Namer { typer: Typer =>
20522057
if paramss.isEmpty then info.widenExpr
20532058
else NoType
20542059

2055-
val iRawInfo =
2056-
cls.info.nonPrivateDecl(sym.name).matchingDenotation(site, schema, sym.targetName).info
2060+
val iDenot = cls.info.nonPrivateDecl(sym.name).matchingDenotation(site, schema, sym.targetName)
2061+
val iSym = iDenot.symbol
2062+
if iSym.is(Tracked) then isTracked = true
2063+
val iRawInfo = iDenot.info
20572064
val iResType = instantiatedResType(iRawInfo, paramss).asSeenFrom(site, cls)
20582065
if (iResType.exists)
20592066
typr.println(i"using inherited type for ${mdef.name}; raw: $iRawInfo, inherited: $iResType")
@@ -2147,6 +2154,7 @@ class Namer { typer: Typer =>
21472154
if defaultTp.exists then TypeOps.SimplifyKeepUnchecked() else null)
21482155
match
21492156
case ctp: ConstantType if sym.isInlineVal => ctp
2157+
case tp if isTracked => tp
21502158
case tp => TypeComparer.widenInferred(tp, pt, Widen.Unions)
21512159

21522160
// Replace aliases to Unit by Unit itself. If we leave the alias in
@@ -2157,7 +2165,7 @@ class Namer { typer: Typer =>
21572165
def lhsType = fullyDefinedType(cookedRhsType, "right-hand side", mdef.srcPos)
21582166
//if (sym.name.toString == "y") println(i"rhs = $rhsType, cooked = $cookedRhsType")
21592167
if (inherited.exists)
2160-
if sym.isInlineVal then lhsType else inherited
2168+
if sym.isInlineVal || isTracked then lhsType else inherited
21612169
else {
21622170
if (sym.is(Implicit))
21632171
mdef match {

Diff for: compiler/src/dotty/tools/dotc/typer/Typer.scala

+6-4
Original file line numberDiff line numberDiff line change
@@ -2433,7 +2433,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
24332433
else if ctx.reporter.errorsReported then UnspecifiedErrorType
24342434
else errorType(em"cannot infer type; expected type $pt is not fully defined", tree.srcPos))
24352435

2436-
def typedTypeTree(tree: untpd.TypeTree, pt: Type)(using Context): Tree =
2436+
def typedTypeTree(tree: untpd.TypeTree, pt: Type)(using Context): Tree = {
24372437
tree match
24382438
case tree: untpd.DerivedTypeTree =>
24392439
tree.ensureCompletions
@@ -2449,6 +2449,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
24492449
}
24502450
case _ =>
24512451
completeTypeTree(InferredTypeTree(), pt, tree)
2452+
}
24522453

24532454
def typedInLambdaTypeTree(tree: untpd.InLambdaTypeTree, pt: Type)(using Context): Tree =
24542455
val tp =
@@ -2860,7 +2861,6 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
28602861
val nnInfo = rhs1.notNullInfo
28612862
vdef1.withNotNullInfo(if sym.is(Lazy) then nnInfo.retractedInfo else nnInfo)
28622863
}
2863-
28642864
private def retractDefDef(sym: Symbol)(using Context): Tree =
28652865
// it's a discarded method (synthetic case class method or synthetic java record constructor or overridden member), drop it
28662866
val canBeInvalidated: Boolean =
@@ -3672,7 +3672,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
36723672
}
36733673

36743674
/** Typecheck and adapt tree, returning a typed tree. Parameters as for `typedUnadapted` */
3675-
def typed(tree: untpd.Tree, pt: Type, locked: TypeVars)(using Context): Tree =
3675+
def typed(tree: untpd.Tree, pt: Type, locked: TypeVars)(using Context): Tree = {
36763676
trace(i"typing $tree, pt = $pt", typr, show = true) {
36773677
record(s"typed $getClass")
36783678
record("typed total")
@@ -3684,6 +3684,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
36843684
tree.withType(WildcardType)
36853685
else adapt(typedUnadapted(tree, pt, locked), pt, locked)
36863686
}
3687+
}
36873688

36883689
def typed(tree: untpd.Tree, pt: Type = WildcardType)(using Context): Tree =
36893690
typed(tree, pt, ctx.typerState.ownedVars)
@@ -3799,7 +3800,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
37993800
def typedExpr(tree: untpd.Tree, pt: Type = WildcardType)(using Context): Tree =
38003801
withoutMode(Mode.PatternOrTypeBits)(typed(tree, pt))
38013802

3802-
def typedType(tree: untpd.Tree, pt: Type = WildcardType, mapPatternBounds: Boolean = false)(using Context): Tree =
3803+
def typedType(tree: untpd.Tree, pt: Type = WildcardType, mapPatternBounds: Boolean = false)(using Context): Tree = {
38033804
val tree1 = withMode(Mode.Type) { typed(tree, pt) }
38043805
if mapPatternBounds && ctx.mode.is(Mode.Pattern) && !ctx.isAfterTyper then
38053806
tree1 match
@@ -3815,6 +3816,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
38153816
case tree1 =>
38163817
tree1
38173818
else tree1
3819+
}
38183820

38193821
def typedPattern(tree: untpd.Tree, selType: Type = WildcardType)(using Context): Tree =
38203822
withMode(Mode.Pattern)(typed(tree, selType))

Diff for: docs/_docs/internals/syntax.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ type val var while with yield
141141
### Soft keywords
142142

143143
```
144-
as derives end erased extension infix inline opaque open throws transparent using | * + -
144+
as derives end erased extension infix inline opaque open throws tracked transparent using | * + -
145145
```
146146

147147
See the [separate section on soft keywords](../reference/soft-modifier.md) for additional
@@ -381,7 +381,7 @@ ClsParamClause ::= [nl] ‘(’ ClsParams ‘)’
381381
| [nl] ‘(’ ‘using’ (ClsParams | FunArgTypes) ‘)’
382382
ClsParams ::= ClsParam {‘,’ ClsParam}
383383
ClsParam ::= {Annotation} ValDef(mods, id, tpe, expr) -- point of mods on val/var
384-
[{Modifier | ‘tracked’} (‘val’ | ‘var’)] Param
384+
[{Modifier} (‘val’ | ‘var’)] Param
385385
386386
DefParamClauses ::= DefParamClause { DefParamClause } -- and two DefTypeParamClause cannot be adjacent
387387
DefParamClause ::= DefTypeParamClause
@@ -418,6 +418,7 @@ LocalModifier ::= ‘abstract’
418418
| ‘transparent’
419419
| ‘infix’
420420
| ‘erased’
421+
| ‘tracked’
421422
422423
AccessModifier ::= (‘private’ | ‘protected’) [AccessQualifier]
423424
AccessQualifier ::= ‘[’ id ‘]’

Diff for: docs/_docs/reference/experimental/modularity.md

+32-8
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,6 @@ This works as it should now. Without the addition of `tracked` to the
108108
parameter of `SetFunctor` typechecking would immediately lose track of
109109
the element type `T` after an `add`, and would therefore fail.
110110

111-
**Syntax Change**
112-
113-
```
114-
ClsParam ::= {Annotation} [{Modifier | ‘tracked’} (‘val’ | ‘var’)] Param
115-
```
116-
117-
The (soft) `tracked` modifier is only allowed for `val` parameters of classes.
118-
119111
**Discussion**
120112

121113
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
134126
scheme is unimplementable since it would quickly lead to cyclic references
135127
when typechecking recursive class graphs. So an explicit `tracked` looks like the best available option.
136128

129+
## Tracked members
130+
131+
The `tracked` modifier can also be used for `val` members of classes and traits
132+
to force the type of the member (or it's overriding member) to be as exact as
133+
possible. More precisely, it will will assign the `tracked` member the infered
134+
type of the rhs. For instance, consider the following definition:
135+
136+
```scala
137+
trait F:
138+
tracked val a: Int
139+
tracked val b: Int
140+
141+
class N extends F:
142+
val a = 22 // a.type =:= 22
143+
val b: Int = 22 // b.type =:= Int
144+
tracked val c = 22 // c.type =:= 22
145+
```
146+
147+
Here, the `tracked` modifier ensures that the type of `a` in `N` is `22` and not
148+
`Int`. But the type of `b` is `N` is `Int` since it's explicitly declared as
149+
`Int`. `tracked` members can also be immediately initialized, as in the case of
150+
`c`.
151+
152+
## Tracked syntax change
153+
154+
```
155+
LocalModifier ::= ‘tracked’
156+
```
157+
158+
The (soft) `tracked` modifier is allowed as a local modifier.
159+
160+
137161
## Allow Class Parents to be Refined Types
138162

139163
Since `tracked` parameters create refinements in constructor types,

Diff for: tests/neg/abstract-tracked-1.scala

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import scala.language.experimental.modularity
2+
import scala.language.future
3+
4+
trait F:
5+
tracked val a: Int
6+
7+
class G:
8+
val a: Int = 1
9+
10+
def Test =
11+
val g = new G
12+
summon[g.a.type <:< 1] // error

Diff for: tests/neg/abstract-tracked.check

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-- [E156] Syntax Error: tests/neg/abstract-tracked.scala:4:14 ----------------------------------------------------------
2+
4 |tracked trait F // error
3+
|^^^^^^^^^^^^^^^
4+
|Modifier tracked is not allowed for this definition
5+
-- [E156] Syntax Error: tests/neg/abstract-tracked.scala:9:15 ----------------------------------------------------------
6+
9 |tracked object O // error
7+
|^^^^^^^^^^^^^^^^
8+
|Modifier tracked is not allowed for this definition
9+
-- [E156] Syntax Error: tests/neg/abstract-tracked.scala:11:14 ---------------------------------------------------------
10+
11 |tracked class C // error
11+
|^^^^^^^^^^^^^^^
12+
|Modifier tracked is not allowed for this definition
13+
-- [E156] Syntax Error: tests/neg/abstract-tracked.scala:7:14 ----------------------------------------------------------
14+
7 | tracked def f: F // error
15+
| ^^^^^^^^^^^^^^^^
16+
| Modifier tracked is not allowed for this definition
17+
-- [E156] Syntax Error: tests/neg/abstract-tracked.scala:14:14 ---------------------------------------------------------
18+
14 | tracked val x = 1 // error
19+
| ^^^^^^^^^^^^^^^^^
20+
| Modifier tracked is not allowed for this definition

Diff for: tests/neg/abstract-tracked.scala

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import scala.language.experimental.modularity
2+
import scala.language.future
3+
4+
tracked trait F // error
5+
6+
trait G:
7+
tracked def f: F // error
8+
9+
tracked object O // error
10+
11+
tracked class C // error
12+
13+
def f =
14+
tracked val x = 1 // error

Diff for: tests/neg/tracked.check

+16-34
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,6 @@
66
7 | def foo(tracked a: Int) = // error
77
| ^
88
| ':' expected, but identifier found
9-
-- Error: tests/neg/tracked.scala:8:12 ---------------------------------------------------------------------------------
10-
8 | tracked val b: Int = 2 // error
11-
| ^^^
12-
| end of statement expected but 'val' found
13-
-- Error: tests/neg/tracked.scala:11:10 --------------------------------------------------------------------------------
14-
11 | tracked object Foo // error // error
15-
| ^^^^^^
16-
| end of statement expected but 'object' found
17-
-- Error: tests/neg/tracked.scala:14:10 --------------------------------------------------------------------------------
18-
14 | tracked class D // error // error
19-
| ^^^^^
20-
| end of statement expected but 'class' found
21-
-- Error: tests/neg/tracked.scala:17:10 --------------------------------------------------------------------------------
22-
17 | tracked type T = Int // error // error
23-
| ^^^^
24-
| end of statement expected but 'type' found
259
-- Error: tests/neg/tracked.scala:20:25 --------------------------------------------------------------------------------
2610
20 | given g2: (tracked val x: Int) => C = C(x) // error
2711
| ^^^^^^^^^^^^^^^^^^
@@ -30,21 +14,19 @@
3014
4 |class C2(tracked var x: Int) // error
3115
| ^
3216
| mutable variables may not be `tracked`
33-
-- [E006] Not Found Error: tests/neg/tracked.scala:11:2 ----------------------------------------------------------------
34-
11 | tracked object Foo // error // error
35-
| ^^^^^^^
36-
| Not found: tracked
37-
|
38-
| longer explanation available when compiling with `-explain`
39-
-- [E006] Not Found Error: tests/neg/tracked.scala:14:2 ----------------------------------------------------------------
40-
14 | tracked class D // error // error
41-
| ^^^^^^^
42-
| Not found: tracked
43-
|
44-
| longer explanation available when compiling with `-explain`
45-
-- [E006] Not Found Error: tests/neg/tracked.scala:17:2 ----------------------------------------------------------------
46-
17 | tracked type T = Int // error // error
47-
| ^^^^^^^
48-
| Not found: tracked
49-
|
50-
| longer explanation available when compiling with `-explain`
17+
-- [E156] Syntax Error: tests/neg/tracked.scala:8:16 -------------------------------------------------------------------
18+
8 | tracked val b: Int = 2 // error
19+
| ^^^^^^^^^^^^^^^^^^^^^^
20+
| Modifier tracked is not allowed for this definition
21+
-- [E156] Syntax Error: tests/neg/tracked.scala:11:17 ------------------------------------------------------------------
22+
11 | tracked object Foo // error
23+
| ^^^^^^^^^^^^^^^^^^
24+
| Modifier tracked is not allowed for this definition
25+
-- [E156] Syntax Error: tests/neg/tracked.scala:14:16 ------------------------------------------------------------------
26+
14 | tracked class D // error
27+
| ^^^^^^^^^^^^^^^
28+
| Modifier tracked is not allowed for this definition
29+
-- [E156] Syntax Error: tests/neg/tracked.scala:17:15 ------------------------------------------------------------------
30+
17 | tracked type T = Int // error
31+
| ^^^^^^^^^^^^^^^^^^^^
32+
| Modifier tracked is not allowed for this definition

Diff for: tests/neg/tracked.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ object A:
88
tracked val b: Int = 2 // error
99

1010
object B:
11-
tracked object Foo // error // error
11+
tracked object Foo // error
1212

1313
object C:
14-
tracked class D // error // error
14+
tracked class D // error
1515

1616
object D:
17-
tracked type T = Int // error // error
17+
tracked type T = Int // error
1818

1919
object E:
2020
given g2: (tracked val x: Int) => C = C(x) // error

0 commit comments

Comments
 (0)