Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement tracked members #21761

Merged
merged 12 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1007,12 +1007,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 @@ -1480,6 +1481,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 @@ -1505,6 +1512,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 @@ -3489,7 +3489,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 @@ -3527,8 +3527,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
22 changes: 15 additions & 7 deletions compiler/src/dotty/tools/dotc/typer/Namer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -877,16 +877,16 @@ class Namer { typer: Typer =>
protected def addAnnotations(sym: Symbol): Unit = original match {
case original: untpd.MemberDef =>
lazy val annotCtx = annotContext(original, sym)
original.setMods:
original.setMods:
original.mods.withAnnotations :
KacperFKorban marked this conversation as resolved.
Show resolved Hide resolved
original.mods.annotations.mapConserve: annotTree =>
original.mods.annotations.mapConserve: annotTree =>
val cls = typedAheadAnnotationClass(annotTree)(using annotCtx)
if (cls eq sym)
report.error(em"An annotation class cannot be annotated with iself", annotTree.srcPos)
annotTree
else
val ann =
if cls.is(JavaDefined) then Checking.checkNamedArgumentForJavaAnnotation(annotTree, cls.asClass)
val ann =
if cls.is(JavaDefined) then Checking.checkNamedArgumentForJavaAnnotation(annotTree, cls.asClass)
else annotTree
val ann1 = Annotation.deferred(cls)(typedAheadExpr(ann)(using annotCtx))
sym.addAnnotation(ann1)
Expand Down Expand Up @@ -2016,6 +2016,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 @@ -2051,8 +2056,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 @@ -2141,6 +2148,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 @@ -2151,7 +2159,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 @@ -2411,7 +2411,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 @@ -2427,6 +2427,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 @@ -2837,7 +2838,6 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
postProcessInfo(vdef1, sym)
vdef1.setDefTree
}

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 @@ -3628,7 +3628,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 @@ -3640,6 +3640,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 @@ -3755,7 +3756,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 @@ -3771,6 +3772,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
Loading
Loading