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

Inferring tracked #21628

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2ec3d68
A PoC for infering tracked with one working case
KacperFKorban Sep 18, 2024
4115578
Check for parameter references in type bounds when infering tracked
KacperFKorban Sep 23, 2024
9d2a245
Try potential fixes for some of the cyclic referenc errors
KacperFKorban Sep 23, 2024
50d8d88
Add non-infering completers for infering tracked
KacperFKorban Oct 23, 2024
07c62f2
Some condition reorder fixes related to infering tracked
KacperFKorban Oct 24, 2024
0499af6
Fix some pickling errors •ᴗ•
KacperFKorban Oct 25, 2024
4e6a6f4
Enable modularity by default to test infering tracked
KacperFKorban Oct 28, 2024
df1048f
Separate features related to tracked into a separate sub-feature
KacperFKorban Dec 10, 2024
2e68fb9
Enable tracked by default to test infering tracked
KacperFKorban Oct 28, 2024
fca3a65
Disable tracked by default, mark accessors as tracked, when infering …
KacperFKorban Oct 29, 2024
abc41f6
Revert "Separate features related to tracked into a separate sub-feat…
KacperFKorban Nov 1, 2024
5c08c56
Refactor checking for symbol references in signatures, when infering …
KacperFKorban Nov 1, 2024
161f697
Also check type members, when infering tracked
KacperFKorban Nov 15, 2024
5e295b3
Cleanup infer-tracked
KacperFKorban Nov 15, 2024
0e453a1
Add a section about tracked inference to the modularity doc
KacperFKorban Nov 18, 2024
c027987
Add some test cases with current limitations
KacperFKorban Nov 18, 2024
9231b69
Infer tracked for explicit type class witnesses
KacperFKorban Nov 29, 2024
7442d5a
Don't add tracked to PrivateLocal witnesses
KacperFKorban Dec 6, 2024
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
8 changes: 4 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ on:
- cron: '0 3 * * *' # Every day at 3 AM
workflow_dispatch:

# Cancels any in-progress runs within the same group identified by workflow name and GH reference (branch or tag)
# Cancels any in-progress runs within the same group identified by workflow name and GH reference (branch or tag)
# For example it would:
# - terminate previous PR CI execution after pushing more changes to the same PR branch
# - terminate previous on-push CI run after merging new PR to main
# - terminate previous on-push CI run after merging new PR to main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
Expand Down Expand Up @@ -831,7 +831,7 @@ jobs:
path: .
- name: Prepare MSI package
shell: bash
run: |
run: |
msiInstaller="scala3-${{ env.RELEASE_TAG }}.msi"
mv scala.msi "${msiInstaller}"
sha256sum "${msiInstaller}" > "${msiInstaller}.sha256"
Expand Down Expand Up @@ -870,7 +870,7 @@ jobs:
# ${upload("tar.gz archive", s"$filename.tar.gz", "application/gzip", distribution)}
# ${upload("tar.gz archive SHA", s"$filename.tar.gz.sha256", "text/plain", distribution)}
# """
# def uploadMSI() =
# def uploadMSI() =
# val distribution = "Windows x86_64 MSI"
# s"""
# # $distribution
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1052,8 +1052,8 @@ object desugar {
// we can reuse the constructor parameters; no derived params are needed.
DefDef(
className.toTermName, joinParams(constrTparams, defParamss), classTypeRef, creatorExpr
) .withMods(companionMods | mods.flags.toTermFlags & (GivenOrImplicit | Inline) | finalFlag)
.withSpan(cdef.span) :: Nil
).withMods(companionMods | mods.flags.toTermFlags & (GivenOrImplicit | Inline) | finalFlag)
.withSpan(cdef.span) :: Nil
}

val self1 = {
Expand Down
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/core/SymDenotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2752,6 +2752,9 @@ 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 isNonInfering: Boolean = false

def apply(sym: Symbol): LazyType = this
def apply(module: TermSymbol, modcls: ClassSymbol): LazyType = this

Expand Down
136 changes: 104 additions & 32 deletions compiler/src/dotty/tools/dotc/typer/Namer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,9 @@ 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 isNonInferingTree(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 @@ -291,6 +294,8 @@ class Namer { typer: Typer =>

val completer = tree match
case tree: TypeDef => TypeDefCompleter(tree)(cctx)
case tree: ValOrDefDef if Feature.enabled(Feature.modularity) && isNonInferingTree(tree) =>
NonInferingCompleter(tree)(cctx)
case _ => Completer(tree)(cctx)
val info = adjustIfModule(completer, tree)
createOrRefine[Symbol](tree, name, flags, ctx.owner, _ => info,
Expand Down Expand Up @@ -1733,6 +1738,10 @@ class Namer { typer: Typer =>
}
}

class NonInferingCompleter(original: ValOrDefDef)(ictx: Context) extends Completer(original)(ictx) {
override def isNonInfering: Boolean = true
}

/** Possible actions to perform when deciding on a forwarder for a member */
private enum CanForward:
case Yes
Expand Down Expand Up @@ -1782,7 +1791,7 @@ class Namer { typer: Typer =>
sym.owner.typeParams.foreach(_.ensureCompleted())
completeTrailingParamss(constr, sym, indexingCtor = true)
if Feature.enabled(modularity) then
constr.termParamss.foreach(_.foreach(setTracked))
constr.termParamss.foreach(_.foreach(setTrackedConstrParam))

/** The signature of a module valdef.
* This will compute the corresponding module class TypeRef immediately
Expand Down Expand Up @@ -1922,22 +1931,24 @@ class Namer { typer: Typer =>
def wrapRefinedMethType(restpe: Type): Type =
wrapMethType(addParamRefinements(restpe, paramSymss))

def addTrackedIfNeeded(ddef: DefDef, owningSym: Symbol): Unit =
for params <- ddef.termParamss; param <- params do
val psym = symbolOfTree(param)
if needsTracked(psym, param, owningSym) then
psym.setFlag(Tracked)
setParamTrackedWithAccessors(psym, sym.maybeOwner.infoOrCompleter)

if Feature.enabled(modularity) 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
typedAheadType(ddef.tpt, defn.UnitType)
val mt = wrapMethType(effectiveResultType(sym, paramSymss))
if sym.isPrimaryConstructor then checkCaseClassParamDependencies(mt, sym.owner)
mt
else if sym.isAllOf(Given | Method) && Feature.enabled(modularity) then
// set every context bound evidence parameter of a given companion method
// to be tracked, provided it has a type that has an abstract type member.
// Add refinements for all tracked parameters to the result type.
for params <- ddef.termParamss; param <- params do
val psym = symbolOfTree(param)
if needsTracked(psym, param) then psym.setFlag(Tracked)
valOrDefDefSig(ddef, sym, paramSymss, wrapRefinedMethType)
else
valOrDefDefSig(ddef, sym, paramSymss, wrapMethType)
val paramFn = if Feature.enabled(Feature.modularity) && sym.isAllOf(Given | Method) then wrapRefinedMethType else wrapMethType
valOrDefDefSig(ddef, sym, paramSymss, paramFn)
end defDefSig

/** Complete the trailing parameters of a DefDef,
Expand Down Expand Up @@ -1986,36 +1997,97 @@ class Namer { typer: Typer =>
cls.srcPos)
case _ =>

/** Under x.modularity, we add `tracked` to context bound witnesses
* that have abstract type members
private def setParamTrackedWithAccessors(psym: Symbol, ownerTpe: Type)(using Context): Unit =
for acc <- ownerTpe.decls.lookupAll(psym.name) if acc.is(ParamAccessor) do
acc.resetFlag(PrivateLocal)
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
*/
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)
!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
*/
def needsTracked(sym: Symbol, param: ValDef)(using Context) =
!sym.is(Tracked)
&& param.hasAttachment(ContextBoundParam)
&& sym.info.memberNames(abstractTypeNameFilter).nonEmpty

/** Under x.modularity, set every context bound evidence parameter of a class to be tracked,
* provided it has a type that has an abstract type member. Reset private and local flags
* so that the parameter becomes a `val`.
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.isNonInfering => 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)

private def maybeParamAccessors(owner: Symbol, sym: Symbol)(using Context): List[Symbol] = owner.infoOrCompleter match
case info: ClassInfo =>
info.decls.lookupAll(sym.name).filter(d => d.is(ParamAccessor)).toList
case _ => List(sym)

/** Under x.modularity, set every context bound evidence parameter or public
* using parameter of a class to be tracked, provided it has a type that has
* an abstract type member. Reset private and local flags so that the
* parameter becomes a `val`.
*/
def setTracked(param: ValDef)(using Context): Unit =
def setTrackedConstrParam(param: ValDef)(using Context): Unit =
val sym = symbolOfTree(param)
sym.maybeOwner.maybeOwner.infoOrCompleter match
case info: ClassInfo if needsTracked(sym, param) =>
case info: ClassInfo
if !sym.is(Tracked) && isContextBoundWitnessWithAbstractMembers(sym, param, sym.maybeOwner.maybeOwner) =>
typr.println(i"set tracked $param, $sym: ${sym.info} containing ${sym.info.memberNames(abstractTypeNameFilter).toList}")
for acc <- info.decls.lookupAll(sym.name) if acc.is(ParamAccessor) do
acc.resetFlag(PrivateLocal)
acc.setFlag(Tracked)
sym.setFlag(Tracked)
setParamTrackedWithAccessors(sym, info)
case _ =>

def inferredResultType(
mdef: ValOrDefDef,
sym: Symbol,
paramss: List[List[Symbol]],
paramFn: Type => Type,
fallbackProto: Type
)(using Context): Type =
mdef: ValOrDefDef,
sym: Symbol,
paramss: List[List[Symbol]],
paramFn: Type => Type,
fallbackProto: Type
)(using Context): Type =

/** A type for this definition that might be inherited from elsewhere:
* If this is a setter parameter, the corresponding getter type.
Expand Down
31 changes: 31 additions & 0 deletions docs/_docs/reference/experimental/modularity.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,37 @@ ClsParam ::= {Annotation} [{Modifier | ‘tracked’} (‘val’ | ‘var’)]

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

**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

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)
}
}
```
In the example above, `ord` is referenced in the signatures of the public
members of `OrdSet`, so a `tracked` modifier will be inserted automatically.

Another common case is when a context bound has an associated type (i.e. an abstract type member) e.g.
```scala 3
trait TC:
type Self
type T

class Klass[A: {TC as tc}]
```

Here, `tc` is a context bound with an associated type `T`, so `tracked` will be inferred for `tc`.

**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 Down
18 changes: 18 additions & 0 deletions tests/neg/infer-tracked-explicit-witness.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import scala.language.experimental.modularity

trait T:
type Self
type X
def foo: Self

class D[C](using wd: C is T)
class E(using we: Int is T)

def Test =
given w: Int is T:
def foo: Int = 42
type X = Long
val d = D(using w)
summon[d.wd.X =:= Long] // error
val e = E(using w)
summon[e.we.X =:= Long] // error
34 changes: 34 additions & 0 deletions tests/pos/infer-tracked-1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import scala.language.experimental.modularity
import scala.language.future

trait Ordering {
type T
def compare(t1:T, t2: T): Int
}

class SetFunctor(val ord: Ordering) {
type Set = List[ord.T]
def empty: Set = Nil

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)
}
}

object Test {
val orderInt = new Ordering {
type T = Int
def compare(t1: T, t2: T): Int = t1 - t2
}

val IntSet = new SetFunctor(orderInt)
import IntSet.*

def main(args: Array[String]) = {
val set = IntSet.empty.add(6).add(8).add(23)
assert(!set.member(7))
assert(set.member(8))
}
}
18 changes: 18 additions & 0 deletions tests/pos/infer-tracked-explicit-witness.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import scala.language.experimental.modularity

trait T:
type Self
type X
def foo: Self

class D[C](using val wd: C is T)
class E(using val we: Int is T)

def Test =
given w: Int is T:
def foo: Int = 42
type X = Long
val d = D(using w)
summon[d.wd.X =:= Long]
val e = E(using w)
summon[e.we.X =:= Long]
8 changes: 8 additions & 0 deletions tests/pos/infer-tracked-parent-refinements.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import scala.language.experimental.modularity
import scala.language.future

trait WithValue { type Value = Int }

case class Year(value: Int) extends WithValue {
val x: Value = 2
}
Loading
Loading