Skip to content

Commit 746b00b

Browse files
authored
Refactor adaptBoxed (#20525)
- Factor out common decomposition and re-composition methods. - Try to make names more informative
2 parents 7abb3d9 + 171dcd0 commit 746b00b

File tree

3 files changed

+154
-171
lines changed

3 files changed

+154
-171
lines changed

compiler/src/dotty/tools/dotc/cc/CaptureOps.scala

+33
Original file line numberDiff line numberDiff line change
@@ -491,4 +491,37 @@ object ReachCapability extends AnnotatedCapability(defn.ReachCapabilityAnnot)
491491
*/
492492
object MaybeCapability extends AnnotatedCapability(defn.MaybeCapabilityAnnot)
493493

494+
/** An extractor for all kinds of function types as well as method and poly types.
495+
* @return 1st half: The argument types or empty if this is a type function
496+
* 2nd half: The result type
497+
*/
498+
object FunctionOrMethod:
499+
def unapply(tp: Type)(using Context): Option[(List[Type], Type)] = tp match
500+
case defn.FunctionOf(args, res, isContextual) => Some((args, res))
501+
case mt: MethodType => Some((mt.paramInfos, mt.resType))
502+
case mt: PolyType => Some((Nil, mt.resType))
503+
case defn.RefinedFunctionOf(rinfo) => unapply(rinfo)
504+
case _ => None
505+
506+
/** If `tp` is a function or method, a type of the same kind with the given
507+
* argument and result types.
508+
*/
509+
extension (self: Type)
510+
def derivedFunctionOrMethod(argTypes: List[Type], resType: Type)(using Context): Type = self match
511+
case self @ AppliedType(tycon, args) if defn.isNonRefinedFunction(self) =>
512+
val args1 = argTypes :+ resType
513+
if args.corresponds(args1)(_ eq _) then self
514+
else self.derivedAppliedType(tycon, args1)
515+
case self @ defn.RefinedFunctionOf(rinfo) =>
516+
val rinfo1 = rinfo.derivedFunctionOrMethod(argTypes, resType)
517+
if rinfo1 eq rinfo then self
518+
else if rinfo1.isInstanceOf[PolyType] then self.derivedRefinedType(refinedInfo = rinfo1)
519+
else rinfo1.toFunctionType(alwaysDependent = true)
520+
case self: MethodType =>
521+
self.derivedLambdaType(paramInfos = argTypes, resType = resType)
522+
case self: PolyType =>
523+
assert(argTypes.isEmpty)
524+
self.derivedLambdaType(resType = resType)
525+
case _ =>
526+
self
494527

compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala

+121-170
Original file line numberDiff line numberDiff line change
@@ -886,7 +886,7 @@ class CheckCaptures extends Recheck, SymTransformer:
886886
*/
887887
override def checkConformsExpr(actual: Type, expected: Type, tree: Tree, addenda: Addenda)(using Context): Type =
888888
var expected1 = alignDependentFunction(expected, actual.stripCapturing)
889-
val actualBoxed = adaptBoxed(actual, expected1, tree.srcPos)
889+
val actualBoxed = adapt(actual, expected1, tree.srcPos)
890890
//println(i"check conforms $actualBoxed <<< $expected1")
891891

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

990-
inline def inNestedEnv[T](boxed: Boolean)(op: => T): T =
991-
val saved = curEnv
992-
curEnv = Env(curEnv.owner, EnvKind.NestedInOwner, CaptureSet.Var(curEnv.owner), if boxed then null else curEnv)
993-
try op
994-
finally curEnv = saved
995-
996-
/** Adapt function type `actual`, which is `aargs -> ares` (possibly with dependencies)
997-
* to `expected` type.
998-
* It returns the adapted type along with a capture set consisting of the references
999-
* that were additionally captured during adaptation.
1000-
* @param reconstruct how to rebuild the adapted function type
990+
/** Adapt the inner shape type: get the adapted shape type, and the capture set leaked during adaptation
991+
* @param boxed if true we adapt to a boxed expected type
1001992
*/
1002-
def adaptFun(actual: Type, aargs: List[Type], ares: Type, expected: Type,
1003-
covariant: Boolean, boxed: Boolean,
1004-
reconstruct: (List[Type], Type) => Type): (Type, CaptureSet) =
1005-
inNestedEnv(boxed):
1006-
val (eargs, eres) = expected.dealias.stripCapturing match
1007-
case defn.FunctionOf(eargs, eres, _) => (eargs, eres)
1008-
case expected: MethodType => (expected.paramInfos, expected.resType)
1009-
case expected @ RefinedType(_, _, rinfo: MethodType) if defn.isFunctionNType(expected) => (rinfo.paramInfos, rinfo.resType)
1010-
case _ => (aargs.map(_ => WildcardType), WildcardType)
1011-
val aargs1 = aargs.zipWithConserve(eargs) { (aarg, earg) => adapt(aarg, earg, !covariant) }
1012-
val ares1 = adapt(ares, eres, covariant)
1013-
1014-
val resTp =
1015-
if (ares1 eq ares) && (aargs1 eq aargs) then actual
1016-
else reconstruct(aargs1, ares1)
1017-
1018-
(resTp, CaptureSet(curEnv.captured.elems))
1019-
end adaptFun
1020-
1021-
/** Adapt type function type `actual` to the expected type.
1022-
* @see [[adaptFun]]
1023-
*/
1024-
def adaptTypeFun(
1025-
actual: Type, ares: Type, expected: Type,
1026-
covariant: Boolean, boxed: Boolean,
1027-
reconstruct: Type => Type): (Type, CaptureSet) =
1028-
inNestedEnv(boxed):
1029-
val eres = expected.dealias.stripCapturing match
1030-
case defn.PolyFunctionOf(rinfo: PolyType) => rinfo.resType
1031-
case expected: PolyType => expected.resType
1032-
case _ => WildcardType
1033-
1034-
val ares1 = adapt(ares, eres, covariant)
1035-
1036-
val resTp =
1037-
if ares1 eq ares then actual
1038-
else reconstruct(ares1)
1039-
1040-
(resTp, CaptureSet(curEnv.captured.elems))
1041-
end adaptTypeFun
1042-
1043-
def adaptInfo(actual: Type, expected: Type, covariant: Boolean): String =
1044-
val arrow = if covariant then "~~>" else "<~~"
1045-
i"adapting $actual $arrow $expected"
1046-
1047-
def adapt(actual: Type, expected: Type, covariant: Boolean): Type = trace(adaptInfo(actual, expected, covariant), recheckr, show = true):
1048-
if expected.isInstanceOf[WildcardType] then actual
1049-
else
1050-
// Decompose the actual type into the inner shape type, the capture set and the box status
1051-
val styp = if actual.isFromJavaObject then actual else actual.stripCapturing
1052-
val cs = actual.captureSet
1053-
val boxed = actual.isBoxedCapturing
1054-
1055-
// A box/unbox should be inserted, if the actual box status mismatches with the expectation
1056-
val needsAdaptation = boxed != expected.isBoxedCapturing
1057-
// Whether to insert a box or an unbox?
1058-
val insertBox = needsAdaptation && covariant != boxed
1059-
1060-
// Adapt the inner shape type: get the adapted shape type, and the capture set leaked during adaptation
1061-
val (styp1, leaked) = styp match {
1062-
case actual @ AppliedType(tycon, args) if defn.isNonRefinedFunction(actual) =>
1063-
adaptFun(actual, args.init, args.last, expected, covariant, insertBox,
1064-
(aargs1, ares1) => actual.derivedAppliedType(tycon, aargs1 :+ ares1))
1065-
case actual @ defn.RefinedFunctionOf(rinfo: MethodType) =>
1066-
// TODO Find a way to combine handling of generic and dependent function types (here and elsewhere)
1067-
adaptFun(actual, rinfo.paramInfos, rinfo.resType, expected, covariant, insertBox,
1068-
(aargs1, ares1) =>
1069-
rinfo.derivedLambdaType(paramInfos = aargs1, resType = ares1)
1070-
.toFunctionType(alwaysDependent = true))
1071-
case actual: MethodType =>
1072-
adaptFun(actual, actual.paramInfos, actual.resType, expected, covariant, insertBox,
1073-
(aargs1, ares1) =>
1074-
actual.derivedLambdaType(paramInfos = aargs1, resType = ares1))
1075-
case actual @ defn.RefinedFunctionOf(rinfo: PolyType) =>
1076-
adaptTypeFun(actual, rinfo.resType, expected, covariant, insertBox,
1077-
ares1 =>
1078-
val rinfo1 = rinfo.derivedLambdaType(rinfo.paramNames, rinfo.paramInfos, ares1)
1079-
val actual1 = actual.derivedRefinedType(refinedInfo = rinfo1)
1080-
actual1
1081-
)
1082-
case _ =>
1083-
(styp, CaptureSet())
1084-
}
993+
def adaptShape(actualShape: Type, boxed: Boolean): (Type, CaptureSet) = actualShape match
994+
case FunctionOrMethod(aargs, ares) =>
995+
val saved = curEnv
996+
curEnv = Env(
997+
curEnv.owner, EnvKind.NestedInOwner,
998+
CaptureSet.Var(curEnv.owner),
999+
if boxed then null else curEnv)
1000+
try
1001+
val (eargs, eres) = expected.dealias.stripCapturing match
1002+
case FunctionOrMethod(eargs, eres) => (eargs, eres)
1003+
case _ => (aargs.map(_ => WildcardType), WildcardType)
1004+
val aargs1 = aargs.zipWithConserve(eargs):
1005+
adaptBoxed(_, _, pos, !covariant, alwaysConst)
1006+
val ares1 = adaptBoxed(ares, eres, pos, covariant, alwaysConst)
1007+
val resTp =
1008+
if (aargs1 eq aargs) && (ares1 eq ares) then actualShape // optimize to avoid redundant matches
1009+
else actualShape.derivedFunctionOrMethod(aargs1, ares1)
1010+
(resTp, CaptureSet(curEnv.captured.elems))
1011+
finally curEnv = saved
1012+
case _ =>
1013+
(actualShape, CaptureSet())
10851014

1086-
// Capture set of the term after adaptation
1087-
val cs1 =
1088-
if covariant then cs ++ leaked
1089-
else
1090-
if !leaked.subCaptures(cs, frozen = false).isOK then
1091-
report.error(
1092-
em"""$expected cannot be box-converted to $actual
1093-
|since the additional capture set $leaked resulted from box conversion is not allowed in $actual""", pos)
1094-
cs
1095-
1096-
// Compute the adapted type
1097-
def adaptedType(resultBoxed: Boolean) =
1098-
if (styp1 eq styp) && leaked.isAlwaysEmpty && boxed == resultBoxed then actual
1099-
else styp1.capturing(if alwaysConst then CaptureSet(cs1.elems) else cs1).forceBoxStatus(resultBoxed)
1100-
1101-
if needsAdaptation then
1102-
val criticalSet = // the set which is not allowed to have `cap`
1103-
if covariant then cs1 // can't box with `cap`
1104-
else expected.captureSet // can't unbox with `cap`
1105-
if criticalSet.isUniversal && expected.isValueType && !ccConfig.allowUniversalInBoxed then
1106-
// We can't box/unbox the universal capability. Leave `actual` as it is
1107-
// so we get an error in checkConforms. This tends to give better error
1108-
// messages than disallowing the root capability in `criticalSet`.
1109-
if ctx.settings.YccDebug.value then
1110-
println(i"cannot box/unbox $actual vs $expected")
1111-
actual
1112-
else
1113-
if !ccConfig.allowUniversalInBoxed then
1114-
// Disallow future addition of `cap` to `criticalSet`.
1115-
criticalSet.disallowRootCapability { () =>
1116-
report.error(
1117-
em"""$actual cannot be box-converted to $expected
1118-
|since one of their capture sets contains the root capability `cap`""",
1119-
pos)
1120-
}
1121-
if !insertBox then // unboxing
1122-
//debugShowEnvs()
1123-
markFree(criticalSet, pos)
1124-
adaptedType(!boxed)
1015+
def adaptStr = i"adapting $actual ${if covariant then "~~>" else "<~~"} $expected"
1016+
1017+
if expected.isInstanceOf[WildcardType] then actual
1018+
else trace(adaptStr, recheckr, show = true):
1019+
// Decompose the actual type into the inner shape type, the capture set and the box status
1020+
val actualShape = if actual.isFromJavaObject then actual else actual.stripCapturing
1021+
val actualIsBoxed = actual.isBoxedCapturing
1022+
1023+
// A box/unbox should be inserted, if the actual box status mismatches with the expectation
1024+
val needsAdaptation = actualIsBoxed != expected.isBoxedCapturing
1025+
// Whether to insert a box or an unbox?
1026+
val insertBox = needsAdaptation && covariant != actualIsBoxed
1027+
1028+
// Adapt the inner shape type: get the adapted shape type, and the capture set leaked during adaptation
1029+
val (adaptedShape, leaked) = adaptShape(actualShape, insertBox)
1030+
1031+
// Capture set of the term after adaptation
1032+
val captures =
1033+
val cs = actual.captureSet
1034+
if covariant then cs ++ leaked
1035+
else
1036+
if !leaked.subCaptures(cs, frozen = false).isOK then
1037+
report.error(
1038+
em"""$expected cannot be box-converted to $actual
1039+
|since the additional capture set $leaked resulted from box conversion is not allowed in $actual""", pos)
1040+
cs
1041+
1042+
// Compute the adapted type
1043+
def adaptedType(resultBoxed: Boolean) =
1044+
if (adaptedShape eq actualShape) && leaked.isAlwaysEmpty && actualIsBoxed == resultBoxed
1045+
then actual
1046+
else adaptedShape
1047+
.capturing(if alwaysConst then CaptureSet(captures.elems) else captures)
1048+
.forceBoxStatus(resultBoxed)
1049+
1050+
if needsAdaptation then
1051+
val criticalSet = // the set which is not allowed to have `cap`
1052+
if covariant then captures // can't box with `cap`
1053+
else expected.captureSet // can't unbox with `cap`
1054+
if criticalSet.isUniversal && expected.isValueType && !ccConfig.allowUniversalInBoxed then
1055+
// We can't box/unbox the universal capability. Leave `actual` as it is
1056+
// so we get an error in checkConforms. This tends to give better error
1057+
// messages than disallowing the root capability in `criticalSet`.
1058+
if ctx.settings.YccDebug.value then
1059+
println(i"cannot box/unbox $actual vs $expected")
1060+
actual
11251061
else
1126-
adaptedType(boxed)
1127-
end adapt
1062+
if !ccConfig.allowUniversalInBoxed then
1063+
// Disallow future addition of `cap` to `criticalSet`.
1064+
criticalSet.disallowRootCapability { () =>
1065+
report.error(
1066+
em"""$actual cannot be box-converted to $expected
1067+
|since one of their capture sets contains the root capability `cap`""",
1068+
pos)
1069+
}
1070+
if !insertBox then // unboxing
1071+
//debugShowEnvs()
1072+
markFree(criticalSet, pos)
1073+
adaptedType(!actualIsBoxed)
1074+
else
1075+
adaptedType(actualIsBoxed)
1076+
end adaptBoxed
11281077

1129-
/** If result derives from caps.Capability, yet is not a capturing type itself,
1130-
* make its capture set explicit.
1131-
*/
1132-
def makeCaptureSetExplicit(result: Type) = result match
1133-
case CapturingType(_, _) => result
1134-
case _ =>
1135-
if result.derivesFromCapability then
1136-
val cap: CaptureRef = actual match
1137-
case ref: CaptureRef if ref.isTracked =>
1138-
ref
1139-
case _ =>
1140-
defn.captureRoot.termRef // TODO: skolemize?
1141-
CapturingType(result, cap.singletonCaptureSet)
1142-
else result
1078+
/** If actual derives from caps.Capability, yet is not a capturing type itself,
1079+
* make its capture set explicit.
1080+
*/
1081+
private def makeCaptureSetExplicit(actual: Type)(using Context): Type = actual match
1082+
case CapturingType(_, _) => actual
1083+
case _ if actual.derivesFromCapability =>
1084+
val cap: CaptureRef = actual match
1085+
case ref: CaptureRef if ref.isTracked => ref
1086+
case _ => defn.captureRoot.termRef // TODO: skolemize?
1087+
CapturingType(actual, cap.singletonCaptureSet)
1088+
case _ => actual
1089+
1090+
/** If actual is a tracked CaptureRef `a` and widened is a capturing type T^C,
1091+
* improve `T^C` to `T^{a}`, following the VAR rule of CC.
1092+
*/
1093+
private def improveCaptures(widened: Type, actual: Type)(using Context): Type = actual match
1094+
case ref: CaptureRef if ref.isTracked =>
1095+
widened match
1096+
case CapturingType(p, refs) if ref.singletonCaptureSet.mightSubcapture(refs) =>
1097+
widened.derivedCapturingType(p, ref.singletonCaptureSet)
1098+
.showing(i"improve $widened to $result", capt)
1099+
case _ => widened
1100+
case _ => widened
11431101

1102+
/** Adapt `actual` type to `expected` type by inserting boxing and unboxing conversions
1103+
*
1104+
* @param alwaysConst always make capture set variables constant after adaptation
1105+
*/
1106+
def adapt(actual: Type, expected: Type, pos: SrcPos)(using Context): Type =
11441107
if expected == LhsProto || expected.isSingleton && actual.isSingleton then
11451108
actual
11461109
else
1147-
var actualw = actual.widenDealias
1148-
actual match
1149-
case ref: CaptureRef if ref.isTracked =>
1150-
actualw match
1151-
case CapturingType(p, refs) if ref.singletonCaptureSet.mightSubcapture(refs) =>
1152-
actualw = actualw.derivedCapturingType(p, ref.singletonCaptureSet)
1153-
.showing(i"improve $actualw to $result", capt)
1154-
// given `a: T^C`, improve `T^C` to `T^{a}`
1155-
case _ =>
1156-
case _ =>
1157-
val adapted = adapt(actualw.withReachCaptures(actual), expected, covariant = true)
1158-
makeCaptureSetExplicit:
1159-
if adapted ne actualw then
1160-
capt.println(i"adapt boxed $actual vs $expected ===> $adapted")
1161-
adapted
1162-
else
1163-
actual
1164-
end adaptBoxed
1110+
val normalized = makeCaptureSetExplicit(actual)
1111+
val widened = improveCaptures(normalized.widenDealias, actual)
1112+
val adapted = adaptBoxed(widened.withReachCaptures(actual), expected, pos, covariant = true, alwaysConst = false)
1113+
if adapted eq widened then normalized
1114+
else adapted.showing(i"adapt boxed $actual vs $expected ===> $adapted", capt)
1115+
end adapt
11651116

11661117
/** Check overrides again, taking capture sets into account.
11671118
* TODO: Can we avoid doing overrides checks twice?
@@ -1180,7 +1131,7 @@ class CheckCaptures extends Recheck, SymTransformer:
11801131
val saved = curEnv
11811132
try
11821133
curEnv = Env(clazz, EnvKind.NestedInOwner, capturedVars(clazz), outer0 = curEnv)
1183-
val adapted = adaptBoxed(actual, expected1, srcPos, alwaysConst = true)
1134+
val adapted = adaptBoxed(actual, expected1, srcPos, covariant = true, alwaysConst = true)
11841135
actual match
11851136
case _: MethodType =>
11861137
// We remove the capture set resulted from box adaptation for method types,

compiler/src/dotty/tools/dotc/core/TypeUtils.scala

-1
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,5 @@ class TypeUtils:
189189
def stripRefinement: Type = self match
190190
case self: RefinedOrRecType => self.parent.stripRefinement
191191
case seld => self
192-
193192
end TypeUtils
194193

0 commit comments

Comments
 (0)