Skip to content

Commit 9855fd4

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 f5edbf7 commit 9855fd4

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

+595
-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: 82 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
@@ -89,7 +90,7 @@ class Definitions {
8990
*
9091
* FunctionN traits follow this template:
9192
*
92-
* trait FunctionN[T0,...T{N-1}, R] extends Object {
93+
* trait FunctionN[-T0,...-T{N-1}, +R] extends Object {
9394
* def apply($x0: T0, ..., $x{N_1}: T{N-1}): R
9495
* }
9596
*
@@ -99,46 +100,65 @@ class Definitions {
99100
*
100101
* ContextFunctionN traits follow this template:
101102
*
102-
* trait ContextFunctionN[T0,...,T{N-1}, R] extends Object {
103+
* trait ContextFunctionN[-T0,...,-T{N-1}, +R] extends Object {
103104
* def apply(using $x0: T0, ..., $x{N_1}: T{N-1}): R
104105
* }
105106
*
106107
* ErasedFunctionN traits follow this template:
107108
*
108-
* trait ErasedFunctionN[T0,...,T{N-1}, R] extends Object {
109+
* trait ErasedFunctionN[-T0,...,-T{N-1}, +R] extends Object {
109110
* def apply(erased $x0: T0, ..., $x{N_1}: T{N-1}): R
110111
* }
111112
*
112113
* ErasedContextFunctionN traits follow this template:
113114
*
114-
* trait ErasedContextFunctionN[T0,...,T{N-1}, R] extends Object {
115+
* trait ErasedContextFunctionN[-T0,...,-T{N-1}, +R] extends Object {
115116
* def apply(using erased $x0: T0, ..., $x{N_1}: T{N-1}): R
116117
* }
117118
*
118119
* ErasedFunctionN and ErasedContextFunctionN erase to Function0.
120+
*
121+
* EffXYZFunctionN afollow this template:
122+
*
123+
* type EffXYZFunctionN[-T0,...,-T{N-1}, +R] = {*} XYZFunctionN[T0,...,T{N-1}, R]
119124
*/
120-
def newFunctionNTrait(name: TypeName): ClassSymbol = {
125+
private def newFunctionNType(name: TypeName): Symbol = {
126+
val impure = name.startsWith("Impure")
121127
val completer = new LazyType {
122128
def complete(denot: SymDenotation)(using Context): Unit = {
123-
val cls = denot.asClass.classSymbol
124-
val decls = newScope
125129
val arity = name.functionArity
126-
val paramNamePrefix = tpnme.scala ++ str.NAME_JOIN ++ name ++ str.EXPAND_SEPARATOR
127-
val argParamRefs = List.tabulate(arity) { i =>
128-
enterTypeParam(cls, paramNamePrefix ++ "T" ++ (i + 1).toString, Contravariant, decls).typeRef
129-
}
130-
val resParamRef = enterTypeParam(cls, paramNamePrefix ++ "R", Covariant, decls).typeRef
131-
val methodType = MethodType.companion(
132-
isContextual = name.isContextFunction,
133-
isImplicit = false,
134-
isErased = name.isErasedFunction)
135-
decls.enter(newMethod(cls, nme.apply, methodType(argParamRefs, resParamRef), Deferred))
136-
denot.info =
137-
ClassInfo(ScalaPackageClass.thisType, cls, ObjectType :: Nil, decls)
130+
if impure then
131+
val argParamNames = List.tabulate(arity)(tpnme.syntheticTypeParamName)
132+
val argVariances = List.fill(arity)(Contravariant)
133+
val underlyingName = name.asSimpleName.drop(6)
134+
val underlyingClass = ScalaPackageVal.requiredClass(underlyingName)
135+
denot.info = TypeAlias(
136+
HKTypeLambda(argParamNames :+ "R".toTypeName, argVariances :+ Covariant)(
137+
tl => List.fill(arity + 1)(TypeBounds.empty),
138+
tl => CapturingType(underlyingClass.typeRef.appliedTo(tl.paramRefs),
139+
CaptureSet.universal, boxed = false)
140+
))
141+
else
142+
val cls = denot.asClass.classSymbol
143+
val decls = newScope
144+
val paramNamePrefix = tpnme.scala ++ str.NAME_JOIN ++ name ++ str.EXPAND_SEPARATOR
145+
val argParamRefs = List.tabulate(arity) { i =>
146+
enterTypeParam(cls, paramNamePrefix ++ "T" ++ (i + 1).toString, Contravariant, decls).typeRef
147+
}
148+
val resParamRef = enterTypeParam(cls, paramNamePrefix ++ "R", Covariant, decls).typeRef
149+
val methodType = MethodType.companion(
150+
isContextual = name.isContextFunction,
151+
isImplicit = false,
152+
isErased = name.isErasedFunction)
153+
decls.enter(newMethod(cls, nme.apply, methodType(argParamRefs, resParamRef), Deferred))
154+
denot.info =
155+
ClassInfo(ScalaPackageClass.thisType, cls, ObjectType :: Nil, decls)
138156
}
139157
}
140-
val flags = Trait | NoInits
141-
newPermanentClassSymbol(ScalaPackageClass, name, flags, completer)
158+
if impure then
159+
newPermanentSymbol(ScalaPackageClass, name, EmptyFlags, completer)
160+
else
161+
newPermanentClassSymbol(ScalaPackageClass, name, Trait | NoInits, completer)
142162
}
143163

144164
private def newMethod(cls: ClassSymbol, name: TermName, info: Type, flags: FlagSet = EmptyFlags): TermSymbol =
@@ -212,7 +232,7 @@ class Definitions {
212232
val cls = ScalaPackageVal.moduleClass.asClass
213233
cls.info.decls.openForMutations.useSynthesizer(
214234
name =>
215-
if (name.isTypeName && name.isSyntheticFunction) newFunctionNTrait(name.asTypeName)
235+
if (name.isTypeName && name.isSyntheticFunction) newFunctionNType(name.asTypeName)
216236
else NoSymbol)
217237
cls
218238
}
@@ -1342,39 +1362,54 @@ class Definitions {
13421362
def SpecializedTuple(base: Symbol, args: List[Type])(using Context): Symbol =
13431363
base.owner.requiredClass(base.name.specializedName(args))
13441364

1365+
/** Cached function types of arbitary arities.
1366+
* Function types are created on demand with newFunctionNTrait, which is
1367+
* called from a synthesizer installed in ScalaPackageClass.
1368+
*/
13451369
private class FunType(prefix: String):
13461370
private var classRefs: Array[TypeRef | Null] = new Array(22)
13471371
def apply(n: Int): TypeRef =
13481372
while n >= classRefs.length do
13491373
val classRefs1 = new Array[TypeRef | Null](classRefs.length * 2)
13501374
Array.copy(classRefs, 0, classRefs1, 0, classRefs.length)
13511375
classRefs = classRefs1
1376+
val funName = s"scala.$prefix$n"
13521377
if classRefs(n) == null then
1353-
classRefs(n) = requiredClassRef(prefix + n.toString)
1378+
classRefs(n) =
1379+
if prefix.startsWith("Impure")
1380+
then staticRef(funName.toTypeName).symbol.typeRef
1381+
else requiredClassRef(funName)
13541382
classRefs(n).nn
1355-
1356-
private val erasedContextFunType = FunType("scala.ErasedContextFunction")
1357-
private val contextFunType = FunType("scala.ContextFunction")
1358-
private val erasedFunType = FunType("scala.ErasedFunction")
1359-
private val funType = FunType("scala.Function")
1360-
1361-
def FunctionClass(n: Int, isContextual: Boolean = false, isErased: Boolean = false)(using Context): Symbol =
1362-
( if isContextual && isErased then erasedContextFunType(n)
1363-
else if isContextual then contextFunType(n)
1364-
else if isErased then erasedFunType(n)
1365-
else funType(n)
1366-
).symbol.asClass
1383+
end FunType
1384+
1385+
private def funTypeIdx(isContextual: Boolean, isErased: Boolean, isImpure: Boolean): Int =
1386+
(if isContextual then 1 else 0)
1387+
+ (if isErased then 2 else 0)
1388+
+ (if isImpure then 4 else 0)
1389+
1390+
private val funTypeArray: IArray[FunType] =
1391+
val arr = Array.ofDim[FunType](8)
1392+
val choices = List(false, true)
1393+
for contxt <- choices; erasd <- choices; impure <- choices do
1394+
var str = "Function"
1395+
if contxt then str = "Context" + str
1396+
if erasd then str = "Erased" + str
1397+
if impure then str = "Impure" + str
1398+
arr(funTypeIdx(contxt, erasd, impure)) = FunType(str)
1399+
IArray.unsafeFromArray(arr)
1400+
1401+
def FunctionSymbol(n: Int, isContextual: Boolean = false, isErased: Boolean = false, isImpure: Boolean = false)(using Context): Symbol =
1402+
funTypeArray(funTypeIdx(isContextual, isErased, isImpure))(n).symbol
13671403

13681404
@tu lazy val Function0_apply: Symbol = Function0.requiredMethod(nme.apply)
1369-
@tu lazy val ContextFunction0_apply: Symbol = ContextFunction0.requiredMethod(nme.apply)
13701405

1371-
@tu lazy val Function0: Symbol = FunctionClass(0)
1372-
@tu lazy val Function1: Symbol = FunctionClass(1)
1373-
@tu lazy val Function2: Symbol = FunctionClass(2)
1374-
@tu lazy val ContextFunction0: Symbol = FunctionClass(0, isContextual = true)
1406+
@tu lazy val Function0: Symbol = FunctionSymbol(0)
1407+
@tu lazy val Function1: Symbol = FunctionSymbol(1)
1408+
@tu lazy val Function2: Symbol = FunctionSymbol(2)
1409+
@tu lazy val ContextFunction0: Symbol = FunctionSymbol(0, isContextual = true)
13751410

1376-
def FunctionType(n: Int, isContextual: Boolean = false, isErased: Boolean = false)(using Context): TypeRef =
1377-
FunctionClass(n, isContextual && !ctx.erasedTypes, isErased).typeRef
1411+
def FunctionType(n: Int, isContextual: Boolean = false, isErased: Boolean = false, isImpure: Boolean = false)(using Context): TypeRef =
1412+
FunctionSymbol(n, isContextual && !ctx.erasedTypes, isErased, isImpure).typeRef
13781413

13791414
lazy val PolyFunctionClass = requiredClass("scala.PolyFunction")
13801415
def PolyFunctionType = PolyFunctionClass.typeRef
@@ -1416,6 +1451,10 @@ class Definitions {
14161451
*/
14171452
def isFunctionClass(cls: Symbol): Boolean = scalaClassName(cls).isFunction
14181453

1454+
/** Is a function class, or an impure function type alias */
1455+
def isFunctionSymbol(sym: Symbol): Boolean =
1456+
sym.isType && (sym.owner eq ScalaPackageClass) && sym.name.isFunction
1457+
14191458
/** Is a function class where
14201459
* - FunctionN for N >= 0 and N != XXL
14211460
*/
@@ -1649,7 +1688,7 @@ class Definitions {
16491688

16501689
def isSpecializableFunction(cls: ClassSymbol, paramTypes: List[Type], retType: Type)(using Context): Boolean =
16511690
paramTypes.length <= 2
1652-
&& (cls.derivesFrom(FunctionClass(paramTypes.length)) || isByNameFunctionClass(cls))
1691+
&& (cls.derivesFrom(FunctionSymbol(paramTypes.length)) || isByNameFunctionClass(cls))
16531692
&& isSpecializableFunctionSAM(paramTypes, retType)
16541693

16551694
/** 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
@@ -199,20 +199,25 @@ object NameOps {
199199
else collectDigits(acc * 10 + d, idx + 1)
200200
collectDigits(0, suffixStart + 8)
201201

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

206216
/** Same as `funArity`, except that it returns -1 if the prefix
207217
* is not one of "", "Context", "Erased", "ErasedContext"
208218
*/
209219
private def checkedFunArity(suffixStart: Int): Int =
210-
if suffixStart == 0
211-
|| isPreceded("Context", suffixStart)
212-
|| isPreceded("Erased", suffixStart)
213-
|| isPreceded("ErasedContext", suffixStart)
214-
then funArity(suffixStart)
215-
else -1
220+
if isFunctionPrefix(suffixStart) then funArity(suffixStart) else -1
216221

217222
/** Is a function name, i.e one of FunctionXXL, FunctionN, ContextFunctionN, ErasedFunctionN, ErasedContextFunctionN for N >= 0
218223
*/
@@ -224,19 +229,14 @@ object NameOps {
224229
*/
225230
def isPlainFunction: Boolean = functionArity >= 0
226231

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

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

241241
/** Is a synthetic function name, i.e. one of
242242
* - 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
@@ -726,6 +726,8 @@ object StdNames {
726726
val XOR : N = "^"
727727
val ZAND : N = "&&"
728728
val ZOR : N = "||"
729+
val PUREARROW: N = "->"
730+
val PURECTXARROW: N = "?->"
729731

730732
// unary operators
731733
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
@@ -2496,7 +2496,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
24962496
case tp1: TypeVar if tp1.isInstantiated =>
24972497
tp1.underlying & tp2
24982498
case CapturingType(parent1, refs1, _) =>
2499-
if subCaptures(tp2.captureSet, refs1, frozenConstraint).isOK then
2499+
if subCaptures(tp2.captureSet, refs1, frozen = true).isOK then
25002500
parent1 & tp2
25012501
else
25022502
tp1.derivedCapturingType(parent1 & tp2, refs1)

0 commit comments

Comments
 (0)