Skip to content

Commit

Permalink
More additions to the standard library (#18799)
Browse files Browse the repository at this point in the history
... and fixes to make that happen.
  • Loading branch information
odersky authored Nov 5, 2023
2 parents 5454110 + 5e49b12 commit ef97ee2
Show file tree
Hide file tree
Showing 150 changed files with 35,907 additions and 189 deletions.
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ class TreeTypeMap(
tree1.withType(mapType(tree1.tpe)) match {
case id: Ident =>
if needsSelect(id.tpe) then
ref(id.tpe.asInstanceOf[TermRef]).withSpan(id.span)
try ref(id.tpe.asInstanceOf[TermRef]).withSpan(id.span)
catch case ex: TypeError => super.transform(id)
else
super.transform(id)
case sel: Select =>
Expand Down
12 changes: 11 additions & 1 deletion compiler/src/dotty/tools/dotc/cc/CaptureOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@ extension (tp: Type)
case _: TypeRef | _: AppliedType => tp.typeSymbol.hasAnnotation(defn.CapabilityAnnot)
case _ => false

def isSealed(using Context): Boolean = tp match
case tp: TypeParamRef => tp.underlying.isSealed
case tp: TypeBounds => tp.hi.hasAnnotation(defn.Caps_SealedAnnot)
case tp: TypeRef => tp.symbol.is(Sealed) || tp.info.isSealed // TODO: drop symbol flag?
case _ => false

/** Drop @retains annotations everywhere */
def dropAllRetains(using Context): Type = // TODO we should drop retains from inferred types before unpickling
val tm = new TypeMap:
Expand All @@ -225,7 +231,11 @@ extension (cls: ClassSymbol)
&& bc.givenSelfType.dealiasKeepAnnots.match
case CapturingType(_, refs) => refs.isAlwaysEmpty
case RetainingType(_, refs) => refs.isEmpty
case selfType => selfType.exists && selfType.captureSet.isAlwaysEmpty
case selfType =>
isCaptureChecking // At Setup we have not processed self types yet, so
// unless a self type is explicitly given, we can't tell
// and err on the side of impure.
&& selfType.exists && selfType.captureSet.isAlwaysEmpty

extension (sym: Symbol)

Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/cc/CaptureSet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,7 @@ object CaptureSet:
upper.isAlwaysEmpty || upper.isConst && upper.elems.size == 1 && upper.elems.contains(r1)
if variance > 0 || isExact then upper
else if variance < 0 then CaptureSet.empty
else if ctx.mode.is(Mode.Printing) then upper
else assert(false, i"trying to add $upper from $r via ${tm.getClass} in a non-variant setting")

/** Apply `f` to each element in `xs`, and join result sets with `++` */
Expand Down
187 changes: 148 additions & 39 deletions compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import typer.RefChecks.{checkAllOverrides, checkSelfAgainstParents, OverridingPa
import typer.Checking.{checkBounds, checkAppliedTypesIn}
import typer.ErrorReporting.{Addenda, err}
import typer.ProtoTypes.{AnySelectionProto, LhsProto}
import util.{SimpleIdentitySet, EqHashMap, SrcPos, Property}
import util.{SimpleIdentitySet, EqHashMap, EqHashSet, SrcPos, Property}
import transform.SymUtils.*
import transform.{Recheck, PreRecheck}
import transform.{Recheck, PreRecheck, CapturedVars}
import Recheck.*
import scala.collection.mutable
import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult}
import StdNames.nme
import NameKinds.DefaultGetterName
import NameKinds.{DefaultGetterName, WildcardParamName}
import reporting.trace

/** The capture checker */
Expand Down Expand Up @@ -147,33 +147,49 @@ object CheckCaptures:
private def disallowRootCapabilitiesIn(tp: Type, carrier: Symbol, what: String, have: String, addendum: String, pos: SrcPos)(using Context) =
val check = new TypeTraverser:

private val seen = new EqHashSet[TypeRef]

/** Check that there is at least one method containing carrier and defined
* in the scope of tparam. E.g. this is OK:
* def f[T] = { ... var x: T ... }
* So is this:
* class C[T] { def f() = { class D { var x: T }}}
* But this is not OK:
* class C[T] { object o { var x: T }}
*/
extension (tparam: Symbol) def isParametricIn(carrier: Symbol): Boolean =
val encl = carrier.owner.enclosingMethodOrClass
if encl.isClass then tparam.isParametricIn(encl)
else
def recur(encl: Symbol): Boolean =
if tparam.owner == encl then true
else if encl.isStatic || !encl.exists then false
else recur(encl.owner.enclosingMethodOrClass)
recur(encl)
carrier.exists && {
val encl = carrier.owner.enclosingMethodOrClass
if encl.isClass then tparam.isParametricIn(encl)
else
def recur(encl: Symbol): Boolean =
if tparam.owner == encl then true
else if encl.isStatic || !encl.exists then false
else recur(encl.owner.enclosingMethodOrClass)
recur(encl)
}

def traverse(t: Type) =
t.dealiasKeepAnnots match
case t: TypeRef =>
capt.println(i"disallow $t, $tp, $what, ${t.symbol.is(Sealed)}")
t.info match
case TypeBounds(_, hi)
if !t.symbol.is(Sealed) && !t.symbol.isParametricIn(carrier) =>
if hi.isAny then
report.error(
em"""$what cannot $have $tp since
|that type refers to the type variable $t, which is not sealed.
|$addendum""",
pos)
else
traverse(hi)
case _ =>
traverseChildren(t)
if !seen.contains(t) then
capt.println(i"disallow $t, $tp, $what, ${t.isSealed}")
seen += t
t.info match
case TypeBounds(_, hi) if !t.isSealed && !t.symbol.isParametricIn(carrier) =>
if hi.isAny then
val detailStr =
if t eq tp then "variable"
else i"refers to the type variable $t, which"
report.error(
em"""$what cannot $have $tp since
|that type $detailStr is not sealed.
|$addendum""",
pos)
else
traverse(hi)
case _ =>
traverseChildren(t)
case AnnotatedType(_, ann) if ann.symbol == defn.UncheckedCapturesAnnot =>
()
case t =>
Expand Down Expand Up @@ -260,11 +276,12 @@ class CheckCaptures extends Recheck, SymTransformer:
pos, provenance)

/** Check subcapturing `cs1 <: cs2`, report error on failure */
def checkSubset(cs1: CaptureSet, cs2: CaptureSet, pos: SrcPos, provenance: => String = "")(using Context) =
def checkSubset(cs1: CaptureSet, cs2: CaptureSet, pos: SrcPos,
provenance: => String = "", cs1description: String = "")(using Context) =
checkOK(
cs1.subCaptures(cs2, frozen = false),
if cs1.elems.size == 1 then i"reference ${cs1.elems.toList.head} is not"
else i"references $cs1 are not all",
if cs1.elems.size == 1 then i"reference ${cs1.elems.toList.head}$cs1description is not"
else i"references $cs1$cs1description are not all",
pos, provenance)

/** The current environment */
Expand Down Expand Up @@ -542,10 +559,10 @@ class CheckCaptures extends Recheck, SymTransformer:
val TypeApply(fn, args) = tree
val polyType = atPhase(thisPhase.prev):
fn.tpe.widen.asInstanceOf[TypeLambda]
for case (arg: TypeTree, pinfo, pname) <- args.lazyZip(polyType.paramInfos).lazyZip((polyType.paramNames)) do
if pinfo.bounds.hi.hasAnnotation(defn.Caps_SealedAnnot) then
for case (arg: TypeTree, formal, pname) <- args.lazyZip(polyType.paramRefs).lazyZip((polyType.paramNames)) do
if formal.isSealed then
def where = if fn.symbol.exists then i" in an argument of ${fn.symbol}" else ""
disallowRootCapabilitiesIn(arg.knownType, fn.symbol,
disallowRootCapabilitiesIn(arg.knownType, NoSymbol,
i"Sealed type variable $pname", "be instantiated to",
i"This is often caused by a local capability$where\nleaking as part of its result.",
tree.srcPos)
Expand Down Expand Up @@ -586,13 +603,58 @@ class CheckCaptures extends Recheck, SymTransformer:
openClosures = openClosures.tail
end recheckClosureBlock

/** Maps mutable variables to the symbols that capture them (in the
* CheckCaptures sense, i.e. symbol is referred to from a different method
* than the one it is defined in).
*/
private val capturedBy = util.HashMap[Symbol, Symbol]()

/** Maps anonymous functions appearing as function arguments to
* the function that is called.
*/
private val anonFunCallee = util.HashMap[Symbol, Symbol]()

/** Populates `capturedBy` and `anonFunCallee`. Called by `checkUnit`.
*/
private def collectCapturedMutVars(using Context) = new TreeTraverser:
def traverse(tree: Tree)(using Context) = tree match
case id: Ident =>
val sym = id.symbol
if sym.is(Mutable, butNot = Method) && sym.owner.isTerm then
val enclMeth = ctx.owner.enclosingMethod
if sym.enclosingMethod != enclMeth then
capturedBy(sym) = enclMeth
case Apply(fn, args) =>
for case closureDef(mdef) <- args do
anonFunCallee(mdef.symbol) = fn.symbol
traverseChildren(tree)
case Inlined(_, bindings, expansion) =>
traverse(bindings)
traverse(expansion)
case mdef: DefDef =>
if !mdef.symbol.isInlineMethod then traverseChildren(tree)
case _ =>
traverseChildren(tree)

override def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Type =
try
if sym.is(Module) then sym.info // Modules are checked by checking the module class
else
if sym.is(Mutable) && !sym.hasAnnotation(defn.UncheckedCapturesAnnot) then
disallowRootCapabilitiesIn(tree.tpt.knownType, sym,
i"mutable $sym", "have type", "", sym.srcPos)
val (carrier, addendum) = capturedBy.get(sym) match
case Some(encl) =>
val enclStr =
if encl.isAnonymousFunction then
val location = anonFunCallee.get(encl) match
case Some(meth) if meth.exists => i" argument in a call to $meth"
case _ => ""
s"an anonymous function$location"
else encl.show
(NoSymbol, i"\nNote that $sym does not count as local since it is captured by $enclStr")
case _ =>
(sym, "")
disallowRootCapabilitiesIn(
tree.tpt.knownType, carrier, i"Mutable $sym", "have type", addendum, sym.srcPos)
checkInferredResult(super.recheckValDef(tree, sym), tree)
finally
if !sym.is(Param) then
Expand Down Expand Up @@ -680,9 +742,15 @@ class CheckCaptures extends Recheck, SymTransformer:
if !param.hasAnnotation(defn.ConstructorOnlyAnnot) then
checkSubset(param.termRef.captureSet, thisSet, param.srcPos) // (3)
for pureBase <- cls.pureBaseClass do // (4)
def selfType = impl.body
.collect:
case TypeDef(tpnme.SELF, rhs) => rhs
.headOption
.getOrElse(tree)
.orElse(tree)
checkSubset(thisSet,
CaptureSet.empty.withDescription(i"of pure base class $pureBase"),
tree.srcPos)
selfType.srcPos, cs1description = " captured by this self type")
super.recheckClassDef(tree, impl, cls)
finally
curEnv = saved
Expand Down Expand Up @@ -1122,6 +1190,8 @@ class CheckCaptures extends Recheck, SymTransformer:

override def needsCheck(overriding: Symbol, overridden: Symbol)(using Context): Boolean =
!setup.isPreCC(overriding) && !setup.isPreCC(overridden)

override def checkInheritedTraitParameters: Boolean = false
end OverridingPairsCheckerCC

def traverse(t: Tree)(using Context) =
Expand Down Expand Up @@ -1158,11 +1228,12 @@ class CheckCaptures extends Recheck, SymTransformer:
private val setup: SetupAPI = thisPhase.prev.asInstanceOf[Setup]

override def checkUnit(unit: CompilationUnit)(using Context): Unit =
setup.setupUnit(ctx.compilationUnit.tpdTree, completeDef)
setup.setupUnit(unit.tpdTree, completeDef)
collectCapturedMutVars.traverse(unit.tpdTree)

if ctx.settings.YccPrintSetup.value then
val echoHeader = "[[syntax tree at end of cc setup]]"
val treeString = show(ctx.compilationUnit.tpdTree)
val treeString = show(unit.tpdTree)
report.echo(s"$echoHeader\n$treeString\n")

withCaptureSetsExplained:
Expand Down Expand Up @@ -1298,6 +1369,39 @@ class CheckCaptures extends Recheck, SymTransformer:
checker.traverse(tree.knownType)
end healTypeParam

def checkNoLocalRootIn(sym: Symbol, info: Type, pos: SrcPos)(using Context): Unit =
val check = new TypeTraverser:
def traverse(tp: Type) = tp match
case tp: TermRef if tp.isLocalRootCapability =>
if tp.localRootOwner == sym then
report.error(i"local root $tp cannot appear in type of $sym", pos)
case tp: ClassInfo =>
traverseChildren(tp)
for mbr <- tp.decls do
if !mbr.is(Private) then checkNoLocalRootIn(sym, mbr.info, mbr.srcPos)
case _ =>
traverseChildren(tp)
check.traverse(info)

def checkArraysAreSealedIn(tp: Type, pos: SrcPos)(using Context): Unit =
val check = new TypeTraverser:
def traverse(t: Type): Unit =
t match
case AppliedType(tycon, arg :: Nil) if tycon.typeSymbol == defn.ArrayClass =>
if !(pos.span.isSynthetic && ctx.reporter.errorsReported)
&& !arg.typeSymbol.name.is(WildcardParamName)
then
CheckCaptures.disallowRootCapabilitiesIn(arg, NoSymbol,
"Array", "have element type",
"Since arrays are mutable, they have to be treated like variables,\nso their element type must be sealed.",
pos)
traverseChildren(t)
case defn.RefinedFunctionOf(rinfo: MethodType) =>
traverse(rinfo)
case _ =>
traverseChildren(t)
check.traverse(tp)

/** Perform the following kinds of checks
* - Check all explicitly written capturing types for well-formedness using `checkWellFormedPost`.
* - Check that arguments of TypeApplys and AppliedTypes conform to their bounds.
Expand All @@ -1309,10 +1413,11 @@ class CheckCaptures extends Recheck, SymTransformer:
val lctx = tree match
case _: DefTree | _: TypeDef if tree.symbol.exists => ctx.withOwner(tree.symbol)
case _ => ctx
traverseChildren(tree)(using lctx)
check(tree)
trace(i"post check $tree"):
traverseChildren(tree)(using lctx)
check(tree)
def check(tree: Tree)(using Context) = tree match
case t @ TypeApply(fun, args) =>
case TypeApply(fun, args) =>
fun.knownType.widen match
case tl: PolyType =>
val normArgs = args.lazyZip(tl.paramInfos).map: (arg, bounds) =>
Expand All @@ -1321,6 +1426,10 @@ class CheckCaptures extends Recheck, SymTransformer:
checkBounds(normArgs, tl)
args.lazyZip(tl.paramNames).foreach(healTypeParam(_, _, fun.symbol))
case _ =>
case _: ValOrDefDef | _: TypeDef =>
checkNoLocalRootIn(tree.symbol, tree.symbol.info, tree.symbol.srcPos)
case tree: TypeTree =>
checkArraysAreSealedIn(tree.tpe, tree.srcPos)
case _ =>
end check
end checker
Expand Down
5 changes: 4 additions & 1 deletion compiler/src/dotty/tools/dotc/cc/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
tree.symbol match
case cls: ClassSymbol =>
val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo
if (selfInfo eq NoType) || cls.is(ModuleClass) && !cls.isStatic then
if ((selfInfo eq NoType) || cls.is(ModuleClass) && !cls.isStatic)
&& !cls.isPureClass
then
// add capture set to self type of nested classes if no self type is given explicitly.
val newSelfType = CapturingType(cinfo.selfType, CaptureSet.Var(cls))
val ps1 = inContext(ctx.withOwner(cls)):
Expand Down Expand Up @@ -705,4 +707,5 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:

def postCheck()(using Context): Unit =
for chk <- todoAtPostCheck do chk(ctx)
todoAtPostCheck.clear()
end Setup
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1443,7 +1443,7 @@ class Definitions {
/** Base classes that are assumed to be pure for the purposes of capture checking.
* Every class inheriting from a pure baseclass is pure.
*/
@tu lazy val pureBaseClasses = Set(defn.ThrowableClass)
@tu lazy val pureBaseClasses = Set(ThrowableClass, PureClass)

/** Non-inheritable lasses that are assumed to be pure for the purposes of capture checking,
*/
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/Substituters.scala
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ object Substituters:
def apply(tp: Type): Type = substThis(tp, from, to, this)(using mapCtx)
}

final class SubstRecThisMap(from: Type, to: Type)(using Context) extends DeepTypeMap {
final class SubstRecThisMap(from: Type, to: Type)(using Context) extends DeepTypeMap, IdempotentCaptRefMap {
def apply(tp: Type): Type = substRecThis(tp, from, to, this)(using mapCtx)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ trait UniqueMessagePositions extends Reporter {
||
dia.pos.exists
&& !ctx.settings.YshowSuppressedErrors.value
&& (dia.pos.start to dia.pos.end).exists(pos =>
positions.get((ctx.source, pos)).exists(_.hides(dia)))
&& (dia.pos.start to dia.pos.end).exists: offset =>
positions.get((ctx.source, offset)).exists(_.hides(dia))

override def markReported(dia: Diagnostic)(using Context): Unit =
if dia.pos.exists then
for (pos <- dia.pos.start to dia.pos.end)
positions.get(ctx.source, pos) match
for offset <- dia.pos.start to dia.pos.end do
positions.get((ctx.source, offset)) match
case Some(dia1) if dia1.hides(dia) =>
case _ => positions((ctx.source, pos)) = dia
case _ => positions((ctx.source, offset)) = dia
super.markReported(dia)
}
Loading

0 comments on commit ef97ee2

Please sign in to comment.