Skip to content

Commit 8514c22

Browse files
committed
Pure function types -> and ?->
1. Allow `->` and `?->` and function operators, treated like `=>` and `?=>`. 2. under -Ycc treat `->` and `?->` as immutable function types, whereas `A => B` is an alias of `{*} A -> B` and `A ?=> B` is an alias of `{*} A ?-> B`. Closures are unaffected, we still use `=>` for all closures where they are pure or not. Improve printing of capturing types Avoid explicit retains annotations also outside phase cc Generate "Impure" function aliases For every (possibly erased and/or context) function class XFunctionN, generate an alias ImpureXFunctionN in the Scala package defined as type ImpureXFunctionN[...] = {*} XFunctionN[...] Also: - Fix a bug in TypeComparer: glb has to test subCapture in a frozen state - Harden EventuallyCapturingType extractor to not crash on illegal capture sets - Cleanup transformation of inferred types
1 parent 5e0061f commit 8514c22

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+596
-448
lines changed

compiler/src/dotty/tools/dotc/ast/untpd.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,13 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
6969
case class InterpolatedString(id: TermName, segments: List[Tree])(implicit @constructorOnly src: SourceFile)
7070
extends TermTree
7171

72-
/** A function type */
72+
/** A function type or closure */
7373
case class Function(args: List[Tree], body: Tree)(implicit @constructorOnly src: SourceFile) extends Tree {
7474
override def isTerm: Boolean = body.isTerm
7575
override def isType: Boolean = body.isType
7676
}
7777

78-
/** A function type with `implicit`, `erased`, or `given` modifiers */
78+
/** A function type or closure with `implicit`, `erased`, or `given` modifiers */
7979
class FunctionWithMods(args: List[Tree], body: Tree, val mods: Modifiers)(implicit @constructorOnly src: SourceFile)
8080
extends Function(args, body)
8181

@@ -216,6 +216,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
216216
case class Transparent()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Transparent)
217217

218218
case class Infix()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Infix)
219+
220+
case class Impure()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Impure)
219221
}
220222

221223
/** Modifiers and annotations for definitions

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

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@ def retainedElems(tree: Tree)(using Context): List[Tree] = tree match
1717
case Apply(_, Typed(SeqLiteral(elems, _), _) :: Nil) => elems
1818
case _ => Nil
1919

20+
class IllegalCaptureRef(tpe: Type) extends Exception
21+
2022
extension (tree: Tree)
2123

22-
def toCaptureRef(using Context): CaptureRef = tree.tpe.asInstanceOf[CaptureRef]
24+
def toCaptureRef(using Context): CaptureRef = tree.tpe match
25+
case ref: CaptureRef => ref
26+
case tpe => throw IllegalCaptureRef(tpe)
2327

2428
def toCaptureSet(using Context): CaptureSet =
2529
tree.getAttachment(Captures) match
@@ -59,20 +63,6 @@ extension (tp: Type)
5963

6064
def isBoxedCapturing(using Context) = !tp.boxedCaptured.isAlwaysEmpty
6165

62-
def canHaveInferredCapture(using Context): Boolean = tp match
63-
case tp: TypeRef if tp.symbol.isClass =>
64-
!tp.symbol.isValueClass && tp.symbol != defn.AnyClass
65-
case _: TypeVar | _: TypeParamRef =>
66-
false
67-
case tp: TypeProxy =>
68-
tp.superType.canHaveInferredCapture
69-
case tp: AndType =>
70-
tp.tp1.canHaveInferredCapture && tp.tp2.canHaveInferredCapture
71-
case tp: OrType =>
72-
tp.tp1.canHaveInferredCapture || tp.tp2.canHaveInferredCapture
73-
case _ =>
74-
false
75-
7666
def stripCapturing(using Context): Type = tp.dealiasKeepAnnots match
7767
case CapturingType(parent, _, _) =>
7868
parent.stripCapturing

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ sealed abstract class CaptureSet extends Showable:
5656
assert(v.isConst)
5757
Const(v.elems)
5858

59+
final def isUniversal(using Context) =
60+
elems.exists {
61+
case ref: TermRef => ref.symbol == defn.captureRoot
62+
case _ => false
63+
}
64+
5965
/** Cast to variable. @pre: !isConst */
6066
def asVar: Var =
6167
assert(!isConst)

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,22 @@ object CapturingType:
1212
else AnnotatedType(parent, CaptureAnnotation(refs, boxed))
1313

1414
def unapply(tp: AnnotatedType)(using Context): Option[(Type, CaptureSet, Boolean)] =
15-
if ctx.phase == Phases.checkCapturesPhase && tp.annot.symbol == defn.RetainsAnnot then
15+
if ctx.phase == Phases.checkCapturesPhase then EventuallyCapturingType.unapply(tp)
16+
else None
17+
18+
end CapturingType
19+
20+
object EventuallyCapturingType:
21+
22+
def unapply(tp: AnnotatedType)(using Context): Option[(Type, CaptureSet, Boolean)] =
23+
if tp.annot.symbol == defn.RetainsAnnot then
1624
tp.annot match
1725
case ann: CaptureAnnotation => Some((tp.parent, ann.refs, ann.boxed))
18-
case ann => Some((tp.parent, ann.tree.toCaptureSet, ann.tree.isBoxedCapturing))
26+
case ann =>
27+
try Some((tp.parent, ann.tree.toCaptureSet, ann.tree.isBoxedCapturing))
28+
catch case ex: IllegalCaptureRef => None
1929
else None
2030

21-
end CapturingType
31+
end EventuallyCapturingType
32+
33+

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

Lines changed: 83 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Flags._, Scopes._, Decorators._, NameOps._, Periods._, NullOpsDecorator._
88
import unpickleScala2.Scala2Unpickler.ensureConstructor
99
import scala.collection.mutable
1010
import collection.mutable
11+
import Denotations.{SingleDenotation, staticRef}
1112
import util.{SimpleIdentityMap, SourceFile, NoSource}
1213
import typer.ImportInfo.RootRef
1314
import Comments.CommentsContext
@@ -88,7 +89,7 @@ class Definitions {
8889
*
8990
* FunctionN traits follow this template:
9091
*
91-
* trait FunctionN[T0,...T{N-1}, R] extends Object {
92+
* trait FunctionN[-T0,...-T{N-1}, +R] extends Object {
9293
* def apply($x0: T0, ..., $x{N_1}: T{N-1}): R
9394
* }
9495
*
@@ -98,46 +99,65 @@ class Definitions {
9899
*
99100
* ContextFunctionN traits follow this template:
100101
*
101-
* trait ContextFunctionN[T0,...,T{N-1}, R] extends Object {
102+
* trait ContextFunctionN[-T0,...,-T{N-1}, +R] extends Object {
102103
* def apply(using $x0: T0, ..., $x{N_1}: T{N-1}): R
103104
* }
104105
*
105106
* ErasedFunctionN traits follow this template:
106107
*
107-
* trait ErasedFunctionN[T0,...,T{N-1}, R] extends Object {
108+
* trait ErasedFunctionN[-T0,...,-T{N-1}, +R] extends Object {
108109
* def apply(erased $x0: T0, ..., $x{N_1}: T{N-1}): R
109110
* }
110111
*
111112
* ErasedContextFunctionN traits follow this template:
112113
*
113-
* trait ErasedContextFunctionN[T0,...,T{N-1}, R] extends Object {
114+
* trait ErasedContextFunctionN[-T0,...,-T{N-1}, +R] extends Object {
114115
* def apply(using erased $x0: T0, ..., $x{N_1}: T{N-1}): R
115116
* }
116117
*
117118
* ErasedFunctionN and ErasedContextFunctionN erase to Function0.
119+
*
120+
* EffXYZFunctionN afollow this template:
121+
*
122+
* type EffXYZFunctionN[-T0,...,-T{N-1}, +R] = {*} XYZFunctionN[T0,...,T{N-1}, R]
118123
*/
119-
def newFunctionNTrait(name: TypeName): ClassSymbol = {
124+
private def newFunctionNType(name: TypeName): Symbol = {
125+
val impure = name.startsWith("Impure")
120126
val completer = new LazyType {
121127
def complete(denot: SymDenotation)(using Context): Unit = {
122-
val cls = denot.asClass.classSymbol
123-
val decls = newScope
124128
val arity = name.functionArity
125-
val paramNamePrefix = tpnme.scala ++ str.NAME_JOIN ++ name ++ str.EXPAND_SEPARATOR
126-
val argParamRefs = List.tabulate(arity) { i =>
127-
enterTypeParam(cls, paramNamePrefix ++ "T" ++ (i + 1).toString, Contravariant, decls).typeRef
128-
}
129-
val resParamRef = enterTypeParam(cls, paramNamePrefix ++ "R", Covariant, decls).typeRef
130-
val methodType = MethodType.companion(
131-
isContextual = name.isContextFunction,
132-
isImplicit = false,
133-
isErased = name.isErasedFunction)
134-
decls.enter(newMethod(cls, nme.apply, methodType(argParamRefs, resParamRef), Deferred))
135-
denot.info =
136-
ClassInfo(ScalaPackageClass.thisType, cls, ObjectType :: Nil, decls)
129+
if impure then
130+
val argParamNames = List.tabulate(arity)(tpnme.syntheticTypeParamName)
131+
val argVariances = List.fill(arity)(Contravariant)
132+
val underlyingName = name.asSimpleName.drop(6)
133+
val underlyingClass = ScalaPackageVal.requiredClass(underlyingName)
134+
denot.info = TypeAlias(
135+
HKTypeLambda(argParamNames :+ "R".toTypeName, argVariances :+ Covariant)(
136+
tl => List.fill(arity + 1)(TypeBounds.empty),
137+
tl => CapturingType(underlyingClass.typeRef.appliedTo(tl.paramRefs),
138+
CaptureSet.universal, boxed = false)
139+
))
140+
else
141+
val cls = denot.asClass.classSymbol
142+
val decls = newScope
143+
val paramNamePrefix = tpnme.scala ++ str.NAME_JOIN ++ name ++ str.EXPAND_SEPARATOR
144+
val argParamRefs = List.tabulate(arity) { i =>
145+
enterTypeParam(cls, paramNamePrefix ++ "T" ++ (i + 1).toString, Contravariant, decls).typeRef
146+
}
147+
val resParamRef = enterTypeParam(cls, paramNamePrefix ++ "R", Covariant, decls).typeRef
148+
val methodType = MethodType.companion(
149+
isContextual = name.isContextFunction,
150+
isImplicit = false,
151+
isErased = name.isErasedFunction)
152+
decls.enter(newMethod(cls, nme.apply, methodType(argParamRefs, resParamRef), Deferred))
153+
denot.info =
154+
ClassInfo(ScalaPackageClass.thisType, cls, ObjectType :: Nil, decls)
137155
}
138156
}
139-
val flags = Trait | NoInits
140-
newPermanentClassSymbol(ScalaPackageClass, name, flags, completer)
157+
if impure then
158+
newPermanentSymbol(ScalaPackageClass, name, EmptyFlags, completer)
159+
else
160+
newPermanentClassSymbol(ScalaPackageClass, name, Trait | NoInits, completer)
141161
}
142162

143163
private def newMethod(cls: ClassSymbol, name: TermName, info: Type, flags: FlagSet = EmptyFlags): TermSymbol =
@@ -211,7 +231,7 @@ class Definitions {
211231
val cls = ScalaPackageVal.moduleClass.asClass
212232
cls.info.decls.openForMutations.useSynthesizer(
213233
name =>
214-
if (name.isTypeName && name.isSyntheticFunction) newFunctionNTrait(name.asTypeName)
234+
if (name.isTypeName && name.isSyntheticFunction) newFunctionNType(name.asTypeName)
215235
else NoSymbol)
216236
cls
217237
}
@@ -1285,39 +1305,55 @@ class Definitions {
12851305

12861306
@tu lazy val TupleType: Array[TypeRef] = mkArityArray("scala.Tuple", MaxTupleArity, 1)
12871307

1308+
/** Cached function types of arbitary arities.
1309+
* Function types are created on demand with newFunctionNTrait, which is
1310+
* called from a synthesizer installed in ScalaPackageClass.
1311+
*/
12881312
private class FunType(prefix: String):
12891313
private var classRefs: Array[TypeRef] = new Array(22)
1314+
12901315
def apply(n: Int): TypeRef =
12911316
while n >= classRefs.length do
12921317
val classRefs1 = new Array[TypeRef](classRefs.length * 2)
12931318
Array.copy(classRefs, 0, classRefs1, 0, classRefs.length)
12941319
classRefs = classRefs1
1320+
val funName = s"scala.$prefix$n"
12951321
if classRefs(n) == null then
1296-
classRefs(n) = requiredClassRef(prefix + n.toString)
1322+
classRefs(n) =
1323+
if prefix.startsWith("Impure")
1324+
then staticRef(funName.toTypeName).symbol.typeRef
1325+
else requiredClassRef(funName)
12971326
classRefs(n)
1298-
1299-
private val erasedContextFunType = FunType("scala.ErasedContextFunction")
1300-
private val contextFunType = FunType("scala.ContextFunction")
1301-
private val erasedFunType = FunType("scala.ErasedFunction")
1302-
private val funType = FunType("scala.Function")
1303-
1304-
def FunctionClass(n: Int, isContextual: Boolean = false, isErased: Boolean = false)(using Context): Symbol =
1305-
( if isContextual && isErased then erasedContextFunType(n)
1306-
else if isContextual then contextFunType(n)
1307-
else if isErased then erasedFunType(n)
1308-
else funType(n)
1309-
).symbol.asClass
1327+
end FunType
1328+
1329+
private def funTypeIdx(isContextual: Boolean, isErased: Boolean, isImpure: Boolean): Int =
1330+
(if isContextual then 1 else 0)
1331+
+ (if isErased then 2 else 0)
1332+
+ (if isImpure then 4 else 0)
1333+
1334+
private val funTypeArray: IArray[FunType] =
1335+
val arr = Array.ofDim[FunType](8)
1336+
val choices = List(false, true)
1337+
for contxt <- choices; erasd <- choices; impure <- choices do
1338+
var str = "Function"
1339+
if contxt then str = "Context" + str
1340+
if erasd then str = "Erased" + str
1341+
if impure then str = "Impure" + str
1342+
arr(funTypeIdx(contxt, erasd, impure)) = FunType(str)
1343+
IArray.unsafeFromArray(arr)
1344+
1345+
def FunctionSymbol(n: Int, isContextual: Boolean = false, isErased: Boolean = false, isImpure: Boolean = false)(using Context): Symbol =
1346+
funTypeArray(funTypeIdx(isContextual, isErased, isImpure))(n).symbol
13101347

13111348
@tu lazy val Function0_apply: Symbol = Function0.requiredMethod(nme.apply)
1312-
@tu lazy val ContextFunction0_apply: Symbol = ContextFunction0.requiredMethod(nme.apply)
13131349

1314-
@tu lazy val Function0: Symbol = FunctionClass(0)
1315-
@tu lazy val Function1: Symbol = FunctionClass(1)
1316-
@tu lazy val Function2: Symbol = FunctionClass(2)
1317-
@tu lazy val ContextFunction0: Symbol = FunctionClass(0, isContextual = true)
1350+
@tu lazy val Function0: Symbol = FunctionSymbol(0)
1351+
@tu lazy val Function1: Symbol = FunctionSymbol(1)
1352+
@tu lazy val Function2: Symbol = FunctionSymbol(2)
1353+
@tu lazy val ContextFunction0: Symbol = FunctionSymbol(0, isContextual = true)
13181354

1319-
def FunctionType(n: Int, isContextual: Boolean = false, isErased: Boolean = false)(using Context): TypeRef =
1320-
FunctionClass(n, isContextual && !ctx.erasedTypes, isErased).typeRef
1355+
def FunctionType(n: Int, isContextual: Boolean = false, isErased: Boolean = false, isImpure: Boolean = false)(using Context): TypeRef =
1356+
FunctionSymbol(n, isContextual && !ctx.erasedTypes, isErased, isImpure).typeRef
13211357

13221358
lazy val PolyFunctionClass = requiredClass("scala.PolyFunction")
13231359
def PolyFunctionType = PolyFunctionClass.typeRef
@@ -1359,6 +1395,10 @@ class Definitions {
13591395
*/
13601396
def isFunctionClass(cls: Symbol): Boolean = scalaClassName(cls).isFunction
13611397

1398+
/** Is a function class, or an impure function type alias */
1399+
def isFunctionSymbol(sym: Symbol): Boolean =
1400+
sym.isType && (sym.owner eq ScalaPackageClass) && sym.name.isFunction
1401+
13621402
/** Is a function class where
13631403
* - FunctionN for N >= 0 and N != XXL
13641404
*/
@@ -1565,7 +1605,7 @@ class Definitions {
15651605

15661606
def isSpecializableFunction(cls: ClassSymbol, paramTypes: List[Type], retType: Type)(using Context): Boolean =
15671607
paramTypes.length <= 2
1568-
&& (cls.derivesFrom(FunctionClass(paramTypes.length)) || isByNameFunctionClass(cls))
1608+
&& (cls.derivesFrom(FunctionSymbol(paramTypes.length)) || isByNameFunctionClass(cls))
15691609
&& isSpecializableFunctionSAM(paramTypes, retType)
15701610

15711611
/** If the Single Abstract Method of a Function class has this type, is it specializable? */

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,8 @@ object Flags {
314314
/** A Scala 2x super accessor / an unpickled Scala 2.x class */
315315
val (SuperParamAliasOrScala2x @ _, SuperParamAlias @ _, Scala2x @ _) = newFlags(26, "<super-param-alias>", "<scala-2.x>")
316316

317-
/** A parameter with a default value */
318-
val (_, HasDefault @ _, _) = newFlags(27, "<hasdefault>")
317+
/** A parameter with a default value / an impure untpd.Function type */
318+
val (_, HasDefault @ _, Impure @ _) = newFlags(27, "<hasdefault>", "<{*}>")
319319

320320
/** An extension method, or a collective extension instance */
321321
val (Extension @ _, ExtensionMethod @ _, _) = newFlags(28, "<extension>")

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

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -196,20 +196,25 @@ object NameOps {
196196
else collectDigits(acc * 10 + d, idx + 1)
197197
collectDigits(0, suffixStart + 8)
198198

199-
/** name[0..suffixStart) == `str` */
200-
private def isPreceded(str: String, suffixStart: Int) =
201-
str.length == suffixStart && name.firstPart.startsWith(str)
199+
private def isFunctionPrefix(suffixStart: Int, mustHave: String = ""): Boolean =
200+
suffixStart >= 0
201+
&& {
202+
val first = name.firstPart
203+
var found = mustHave.isEmpty
204+
def skip(idx: Int, str: String) =
205+
if first.startsWith(str, idx) then
206+
if str == mustHave then found = true
207+
idx + str.length
208+
else idx
209+
skip(skip(skip(0, "Impure"), "Erased"), "Context") == suffixStart
210+
&& found
211+
}
202212

203213
/** Same as `funArity`, except that it returns -1 if the prefix
204214
* is not one of "", "Context", "Erased", "ErasedContext"
205215
*/
206216
private def checkedFunArity(suffixStart: Int): Int =
207-
if suffixStart == 0
208-
|| isPreceded("Context", suffixStart)
209-
|| isPreceded("Erased", suffixStart)
210-
|| isPreceded("ErasedContext", suffixStart)
211-
then funArity(suffixStart)
212-
else -1
217+
if isFunctionPrefix(suffixStart) then funArity(suffixStart) else -1
213218

214219
/** Is a function name, i.e one of FunctionXXL, FunctionN, ContextFunctionN, ErasedFunctionN, ErasedContextFunctionN for N >= 0
215220
*/
@@ -221,19 +226,14 @@ object NameOps {
221226
*/
222227
def isPlainFunction: Boolean = functionArity >= 0
223228

224-
/** Is an context function name, i.e one of ContextFunctionN or ErasedContextFunctionN for N >= 0
225-
*/
226-
def isContextFunction: Boolean =
229+
/** Is a function name that contains `mustHave` as a substring */
230+
private def isSpecificFunction(mustHave: String): Boolean =
227231
val suffixStart = functionSuffixStart
228-
(isPreceded("Context", suffixStart) || isPreceded("ErasedContext", suffixStart))
229-
&& funArity(suffixStart) >= 0
232+
isFunctionPrefix(suffixStart, mustHave) && funArity(suffixStart) >= 0
230233

231-
/** Is an erased function name, i.e. one of ErasedFunctionN, ErasedContextFunctionN for N >= 0
232-
*/
233-
def isErasedFunction: Boolean =
234-
val suffixStart = functionSuffixStart
235-
(isPreceded("Erased", suffixStart) || isPreceded("ErasedContext", suffixStart))
236-
&& funArity(suffixStart) >= 0
234+
def isContextFunction: Boolean = isSpecificFunction("Context")
235+
def isErasedFunction: Boolean = isSpecificFunction("Erased")
236+
def isImpureFunction: Boolean = isSpecificFunction("Impure")
237237

238238
/** Is a synthetic function name, i.e. one of
239239
* - FunctionN for N > 22

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,8 @@ object StdNames {
738738
val XOR : N = "^"
739739
val ZAND : N = "&&"
740740
val ZOR : N = "||"
741+
val PUREARROW: N = "->"
742+
val PURECTXARROW: N = "?->"
741743

742744
// unary operators
743745
val UNARY_PREFIX: N = "unary_"

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2410,7 +2410,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
24102410
case tp1: TypeVar if tp1.isInstantiated =>
24112411
tp1.underlying & tp2
24122412
case CapturingType(parent1, refs1, _) =>
2413-
if subCaptures(tp2.captureSet, refs1, frozenConstraint).isOK then
2413+
if subCaptures(tp2.captureSet, refs1, frozen = true).isOK then
24142414
parent1 & tp2
24152415
else
24162416
tp1.derivedCapturingType(parent1 & tp2, refs1)

0 commit comments

Comments
 (0)