Skip to content

Commit

Permalink
Implement tracked members (#21761)
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
3 people authored Dec 19, 2024
1 parent 4b7f321 commit 0041987
Show file tree
Hide file tree
Showing 15 changed files with 198 additions and 62 deletions.
12 changes: 10 additions & 2 deletions compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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 =>
Expand All @@ -1586,6 +1593,7 @@ object desugar {
} else tested
tested = checkOpaqueAlias(tested)
tested = checkApplicable(Opaque, legalOpaque)
tested = checkApplicable(Tracked, legalTracked)
tested
case _ =>
tree
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/Flags.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/parsing/Scanners.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
16 changes: 12 additions & 4 deletions compiler/src/dotty/tools/dotc/typer/Namer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down
10 changes: 6 additions & 4 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 =
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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")
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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))
Expand Down
5 changes: 3 additions & 2 deletions docs/_docs/internals/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -418,6 +418,7 @@ LocalModifier ::= ‘abstract’
| ‘transparent’
| ‘infix’
| ‘erased’
| ‘tracked’
AccessModifier ::= (‘private’ | ‘protected’) [AccessQualifier]
AccessQualifier ::= ‘[’ id ‘]’
Expand Down
40 changes: 32 additions & 8 deletions docs/_docs/reference/experimental/modularity.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions tests/neg/abstract-tracked-1.scala
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions tests/neg/abstract-tracked.check
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions tests/neg/abstract-tracked.scala
Original file line number Diff line number Diff line change
@@ -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
50 changes: 16 additions & 34 deletions tests/neg/tracked.check
Original file line number Diff line number Diff line change
Expand Up @@ -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
| ^^^^^^^^^^^^^^^^^^
Expand All @@ -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
6 changes: 3 additions & 3 deletions tests/neg/tracked.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading

0 comments on commit 0041987

Please sign in to comment.