Skip to content

Commit

Permalink
Refactor adaptBoxed (scala#20525)
Browse files Browse the repository at this point in the history
 - Factor out common decomposition and re-composition methods. 
 - Try to make names more informative
  • Loading branch information
odersky committed Jun 5, 2024
2 parents 7abb3d9 + 171dcd0 commit 746b00b
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 171 deletions.
33 changes: 33 additions & 0 deletions compiler/src/dotty/tools/dotc/cc/CaptureOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -491,4 +491,37 @@ object ReachCapability extends AnnotatedCapability(defn.ReachCapabilityAnnot)
*/
object MaybeCapability extends AnnotatedCapability(defn.MaybeCapabilityAnnot)

/** An extractor for all kinds of function types as well as method and poly types.
* @return 1st half: The argument types or empty if this is a type function
* 2nd half: The result type
*/
object FunctionOrMethod:
def unapply(tp: Type)(using Context): Option[(List[Type], Type)] = tp match
case defn.FunctionOf(args, res, isContextual) => Some((args, res))
case mt: MethodType => Some((mt.paramInfos, mt.resType))
case mt: PolyType => Some((Nil, mt.resType))
case defn.RefinedFunctionOf(rinfo) => unapply(rinfo)
case _ => None

/** If `tp` is a function or method, a type of the same kind with the given
* argument and result types.
*/
extension (self: Type)
def derivedFunctionOrMethod(argTypes: List[Type], resType: Type)(using Context): Type = self match
case self @ AppliedType(tycon, args) if defn.isNonRefinedFunction(self) =>
val args1 = argTypes :+ resType
if args.corresponds(args1)(_ eq _) then self
else self.derivedAppliedType(tycon, args1)
case self @ defn.RefinedFunctionOf(rinfo) =>
val rinfo1 = rinfo.derivedFunctionOrMethod(argTypes, resType)
if rinfo1 eq rinfo then self
else if rinfo1.isInstanceOf[PolyType] then self.derivedRefinedType(refinedInfo = rinfo1)
else rinfo1.toFunctionType(alwaysDependent = true)
case self: MethodType =>
self.derivedLambdaType(paramInfos = argTypes, resType = resType)
case self: PolyType =>
assert(argTypes.isEmpty)
self.derivedLambdaType(resType = resType)
case _ =>
self

291 changes: 121 additions & 170 deletions compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,7 @@ class CheckCaptures extends Recheck, SymTransformer:
*/
override def checkConformsExpr(actual: Type, expected: Type, tree: Tree, addenda: Addenda)(using Context): Type =
var expected1 = alignDependentFunction(expected, actual.stripCapturing)
val actualBoxed = adaptBoxed(actual, expected1, tree.srcPos)
val actualBoxed = adapt(actual, expected1, tree.srcPos)
//println(i"check conforms $actualBoxed <<< $expected1")

if actualBoxed eq actual then
Expand Down Expand Up @@ -985,183 +985,134 @@ class CheckCaptures extends Recheck, SymTransformer:
*
* @param alwaysConst always make capture set variables constant after adaptation
*/
def adaptBoxed(actual: Type, expected: Type, pos: SrcPos, alwaysConst: Boolean = false)(using Context): Type =
def adaptBoxed(actual: Type, expected: Type, pos: SrcPos, covariant: Boolean, alwaysConst: Boolean)(using Context): Type =

inline def inNestedEnv[T](boxed: Boolean)(op: => T): T =
val saved = curEnv
curEnv = Env(curEnv.owner, EnvKind.NestedInOwner, CaptureSet.Var(curEnv.owner), if boxed then null else curEnv)
try op
finally curEnv = saved

/** Adapt function type `actual`, which is `aargs -> ares` (possibly with dependencies)
* to `expected` type.
* It returns the adapted type along with a capture set consisting of the references
* that were additionally captured during adaptation.
* @param reconstruct how to rebuild the adapted function type
/** Adapt the inner shape type: get the adapted shape type, and the capture set leaked during adaptation
* @param boxed if true we adapt to a boxed expected type
*/
def adaptFun(actual: Type, aargs: List[Type], ares: Type, expected: Type,
covariant: Boolean, boxed: Boolean,
reconstruct: (List[Type], Type) => Type): (Type, CaptureSet) =
inNestedEnv(boxed):
val (eargs, eres) = expected.dealias.stripCapturing match
case defn.FunctionOf(eargs, eres, _) => (eargs, eres)
case expected: MethodType => (expected.paramInfos, expected.resType)
case expected @ RefinedType(_, _, rinfo: MethodType) if defn.isFunctionNType(expected) => (rinfo.paramInfos, rinfo.resType)
case _ => (aargs.map(_ => WildcardType), WildcardType)
val aargs1 = aargs.zipWithConserve(eargs) { (aarg, earg) => adapt(aarg, earg, !covariant) }
val ares1 = adapt(ares, eres, covariant)

val resTp =
if (ares1 eq ares) && (aargs1 eq aargs) then actual
else reconstruct(aargs1, ares1)

(resTp, CaptureSet(curEnv.captured.elems))
end adaptFun

/** Adapt type function type `actual` to the expected type.
* @see [[adaptFun]]
*/
def adaptTypeFun(
actual: Type, ares: Type, expected: Type,
covariant: Boolean, boxed: Boolean,
reconstruct: Type => Type): (Type, CaptureSet) =
inNestedEnv(boxed):
val eres = expected.dealias.stripCapturing match
case defn.PolyFunctionOf(rinfo: PolyType) => rinfo.resType
case expected: PolyType => expected.resType
case _ => WildcardType

val ares1 = adapt(ares, eres, covariant)

val resTp =
if ares1 eq ares then actual
else reconstruct(ares1)

(resTp, CaptureSet(curEnv.captured.elems))
end adaptTypeFun

def adaptInfo(actual: Type, expected: Type, covariant: Boolean): String =
val arrow = if covariant then "~~>" else "<~~"
i"adapting $actual $arrow $expected"

def adapt(actual: Type, expected: Type, covariant: Boolean): Type = trace(adaptInfo(actual, expected, covariant), recheckr, show = true):
if expected.isInstanceOf[WildcardType] then actual
else
// Decompose the actual type into the inner shape type, the capture set and the box status
val styp = if actual.isFromJavaObject then actual else actual.stripCapturing
val cs = actual.captureSet
val boxed = actual.isBoxedCapturing

// A box/unbox should be inserted, if the actual box status mismatches with the expectation
val needsAdaptation = boxed != expected.isBoxedCapturing
// Whether to insert a box or an unbox?
val insertBox = needsAdaptation && covariant != boxed

// Adapt the inner shape type: get the adapted shape type, and the capture set leaked during adaptation
val (styp1, leaked) = styp match {
case actual @ AppliedType(tycon, args) if defn.isNonRefinedFunction(actual) =>
adaptFun(actual, args.init, args.last, expected, covariant, insertBox,
(aargs1, ares1) => actual.derivedAppliedType(tycon, aargs1 :+ ares1))
case actual @ defn.RefinedFunctionOf(rinfo: MethodType) =>
// TODO Find a way to combine handling of generic and dependent function types (here and elsewhere)
adaptFun(actual, rinfo.paramInfos, rinfo.resType, expected, covariant, insertBox,
(aargs1, ares1) =>
rinfo.derivedLambdaType(paramInfos = aargs1, resType = ares1)
.toFunctionType(alwaysDependent = true))
case actual: MethodType =>
adaptFun(actual, actual.paramInfos, actual.resType, expected, covariant, insertBox,
(aargs1, ares1) =>
actual.derivedLambdaType(paramInfos = aargs1, resType = ares1))
case actual @ defn.RefinedFunctionOf(rinfo: PolyType) =>
adaptTypeFun(actual, rinfo.resType, expected, covariant, insertBox,
ares1 =>
val rinfo1 = rinfo.derivedLambdaType(rinfo.paramNames, rinfo.paramInfos, ares1)
val actual1 = actual.derivedRefinedType(refinedInfo = rinfo1)
actual1
)
case _ =>
(styp, CaptureSet())
}
def adaptShape(actualShape: Type, boxed: Boolean): (Type, CaptureSet) = actualShape match
case FunctionOrMethod(aargs, ares) =>
val saved = curEnv
curEnv = Env(
curEnv.owner, EnvKind.NestedInOwner,
CaptureSet.Var(curEnv.owner),
if boxed then null else curEnv)
try
val (eargs, eres) = expected.dealias.stripCapturing match
case FunctionOrMethod(eargs, eres) => (eargs, eres)
case _ => (aargs.map(_ => WildcardType), WildcardType)
val aargs1 = aargs.zipWithConserve(eargs):
adaptBoxed(_, _, pos, !covariant, alwaysConst)
val ares1 = adaptBoxed(ares, eres, pos, covariant, alwaysConst)
val resTp =
if (aargs1 eq aargs) && (ares1 eq ares) then actualShape // optimize to avoid redundant matches
else actualShape.derivedFunctionOrMethod(aargs1, ares1)
(resTp, CaptureSet(curEnv.captured.elems))
finally curEnv = saved
case _ =>
(actualShape, CaptureSet())

// Capture set of the term after adaptation
val cs1 =
if covariant then cs ++ leaked
else
if !leaked.subCaptures(cs, frozen = false).isOK then
report.error(
em"""$expected cannot be box-converted to $actual
|since the additional capture set $leaked resulted from box conversion is not allowed in $actual""", pos)
cs

// Compute the adapted type
def adaptedType(resultBoxed: Boolean) =
if (styp1 eq styp) && leaked.isAlwaysEmpty && boxed == resultBoxed then actual
else styp1.capturing(if alwaysConst then CaptureSet(cs1.elems) else cs1).forceBoxStatus(resultBoxed)

if needsAdaptation then
val criticalSet = // the set which is not allowed to have `cap`
if covariant then cs1 // can't box with `cap`
else expected.captureSet // can't unbox with `cap`
if criticalSet.isUniversal && expected.isValueType && !ccConfig.allowUniversalInBoxed then
// We can't box/unbox the universal capability. Leave `actual` as it is
// so we get an error in checkConforms. This tends to give better error
// messages than disallowing the root capability in `criticalSet`.
if ctx.settings.YccDebug.value then
println(i"cannot box/unbox $actual vs $expected")
actual
else
if !ccConfig.allowUniversalInBoxed then
// Disallow future addition of `cap` to `criticalSet`.
criticalSet.disallowRootCapability { () =>
report.error(
em"""$actual cannot be box-converted to $expected
|since one of their capture sets contains the root capability `cap`""",
pos)
}
if !insertBox then // unboxing
//debugShowEnvs()
markFree(criticalSet, pos)
adaptedType(!boxed)
def adaptStr = i"adapting $actual ${if covariant then "~~>" else "<~~"} $expected"

if expected.isInstanceOf[WildcardType] then actual
else trace(adaptStr, recheckr, show = true):
// Decompose the actual type into the inner shape type, the capture set and the box status
val actualShape = if actual.isFromJavaObject then actual else actual.stripCapturing
val actualIsBoxed = actual.isBoxedCapturing

// A box/unbox should be inserted, if the actual box status mismatches with the expectation
val needsAdaptation = actualIsBoxed != expected.isBoxedCapturing
// Whether to insert a box or an unbox?
val insertBox = needsAdaptation && covariant != actualIsBoxed

// Adapt the inner shape type: get the adapted shape type, and the capture set leaked during adaptation
val (adaptedShape, leaked) = adaptShape(actualShape, insertBox)

// Capture set of the term after adaptation
val captures =
val cs = actual.captureSet
if covariant then cs ++ leaked
else
if !leaked.subCaptures(cs, frozen = false).isOK then
report.error(
em"""$expected cannot be box-converted to $actual
|since the additional capture set $leaked resulted from box conversion is not allowed in $actual""", pos)
cs

// Compute the adapted type
def adaptedType(resultBoxed: Boolean) =
if (adaptedShape eq actualShape) && leaked.isAlwaysEmpty && actualIsBoxed == resultBoxed
then actual
else adaptedShape
.capturing(if alwaysConst then CaptureSet(captures.elems) else captures)
.forceBoxStatus(resultBoxed)

if needsAdaptation then
val criticalSet = // the set which is not allowed to have `cap`
if covariant then captures // can't box with `cap`
else expected.captureSet // can't unbox with `cap`
if criticalSet.isUniversal && expected.isValueType && !ccConfig.allowUniversalInBoxed then
// We can't box/unbox the universal capability. Leave `actual` as it is
// so we get an error in checkConforms. This tends to give better error
// messages than disallowing the root capability in `criticalSet`.
if ctx.settings.YccDebug.value then
println(i"cannot box/unbox $actual vs $expected")
actual
else
adaptedType(boxed)
end adapt
if !ccConfig.allowUniversalInBoxed then
// Disallow future addition of `cap` to `criticalSet`.
criticalSet.disallowRootCapability { () =>
report.error(
em"""$actual cannot be box-converted to $expected
|since one of their capture sets contains the root capability `cap`""",
pos)
}
if !insertBox then // unboxing
//debugShowEnvs()
markFree(criticalSet, pos)
adaptedType(!actualIsBoxed)
else
adaptedType(actualIsBoxed)
end adaptBoxed

/** If result derives from caps.Capability, yet is not a capturing type itself,
* make its capture set explicit.
*/
def makeCaptureSetExplicit(result: Type) = result match
case CapturingType(_, _) => result
case _ =>
if result.derivesFromCapability then
val cap: CaptureRef = actual match
case ref: CaptureRef if ref.isTracked =>
ref
case _ =>
defn.captureRoot.termRef // TODO: skolemize?
CapturingType(result, cap.singletonCaptureSet)
else result
/** If actual derives from caps.Capability, yet is not a capturing type itself,
* make its capture set explicit.
*/
private def makeCaptureSetExplicit(actual: Type)(using Context): Type = actual match
case CapturingType(_, _) => actual
case _ if actual.derivesFromCapability =>
val cap: CaptureRef = actual match
case ref: CaptureRef if ref.isTracked => ref
case _ => defn.captureRoot.termRef // TODO: skolemize?
CapturingType(actual, cap.singletonCaptureSet)
case _ => actual

/** If actual is a tracked CaptureRef `a` and widened is a capturing type T^C,
* improve `T^C` to `T^{a}`, following the VAR rule of CC.
*/
private def improveCaptures(widened: Type, actual: Type)(using Context): Type = actual match
case ref: CaptureRef if ref.isTracked =>
widened match
case CapturingType(p, refs) if ref.singletonCaptureSet.mightSubcapture(refs) =>
widened.derivedCapturingType(p, ref.singletonCaptureSet)
.showing(i"improve $widened to $result", capt)
case _ => widened
case _ => widened

/** Adapt `actual` type to `expected` type by inserting boxing and unboxing conversions
*
* @param alwaysConst always make capture set variables constant after adaptation
*/
def adapt(actual: Type, expected: Type, pos: SrcPos)(using Context): Type =
if expected == LhsProto || expected.isSingleton && actual.isSingleton then
actual
else
var actualw = actual.widenDealias
actual match
case ref: CaptureRef if ref.isTracked =>
actualw match
case CapturingType(p, refs) if ref.singletonCaptureSet.mightSubcapture(refs) =>
actualw = actualw.derivedCapturingType(p, ref.singletonCaptureSet)
.showing(i"improve $actualw to $result", capt)
// given `a: T^C`, improve `T^C` to `T^{a}`
case _ =>
case _ =>
val adapted = adapt(actualw.withReachCaptures(actual), expected, covariant = true)
makeCaptureSetExplicit:
if adapted ne actualw then
capt.println(i"adapt boxed $actual vs $expected ===> $adapted")
adapted
else
actual
end adaptBoxed
val normalized = makeCaptureSetExplicit(actual)
val widened = improveCaptures(normalized.widenDealias, actual)
val adapted = adaptBoxed(widened.withReachCaptures(actual), expected, pos, covariant = true, alwaysConst = false)
if adapted eq widened then normalized
else adapted.showing(i"adapt boxed $actual vs $expected ===> $adapted", capt)
end adapt

/** Check overrides again, taking capture sets into account.
* TODO: Can we avoid doing overrides checks twice?
Expand All @@ -1180,7 +1131,7 @@ class CheckCaptures extends Recheck, SymTransformer:
val saved = curEnv
try
curEnv = Env(clazz, EnvKind.NestedInOwner, capturedVars(clazz), outer0 = curEnv)
val adapted = adaptBoxed(actual, expected1, srcPos, alwaysConst = true)
val adapted = adaptBoxed(actual, expected1, srcPos, covariant = true, alwaysConst = true)
actual match
case _: MethodType =>
// We remove the capture set resulted from box adaptation for method types,
Expand Down
1 change: 0 additions & 1 deletion compiler/src/dotty/tools/dotc/core/TypeUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,5 @@ class TypeUtils:
def stripRefinement: Type = self match
case self: RefinedOrRecType => self.parent.stripRefinement
case seld => self

end TypeUtils

0 comments on commit 746b00b

Please sign in to comment.