Skip to content

Commit 117fbdc

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.
1 parent a9810c1 commit 117fbdc

Some content is hidden

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

48 files changed

+290
-270
lines changed

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

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

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

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

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

219219
case class Infix()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Infix)
220+
221+
case class Impure()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Impure)
220222
}
221223

222224
/** Modifiers and annotations for definitions

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1381,6 +1381,10 @@ class Definitions {
13811381
*/
13821382
def isFunctionClass(cls: Symbol): Boolean = scalaClassName(cls).isFunction
13831383

1384+
/** Is a function class, or an impure function type alias */
1385+
def isFunctionSymbol(sym: Symbol): Boolean =
1386+
sym.isType && (sym.owner eq ScalaPackageClass) && sym.name.isFunction
1387+
13841388
/** Is a function class where
13851389
* - FunctionN for N >= 0 and N != XXL
13861390
*/

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: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -227,17 +227,14 @@ object NameOps {
227227
*/
228228
def isPlainFunction: Boolean = functionArity >= 0
229229

230-
/** Is an context function name, i.e one of ContextFunctionN or ErasedContextFunctionN for N >= 0
231-
*/
232-
def isContextFunction: Boolean =
230+
/** Is a function name that contains `mustHave` as a substring */
231+
private def isSpecificFunction(mustHave: String): Boolean =
233232
val suffixStart = functionSuffixStart
234-
isFunctionPrefix(suffixStart, mustHave = "Context") && funArity(suffixStart) >= 0
233+
isFunctionPrefix(suffixStart, mustHave) && funArity(suffixStart) >= 0
235234

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

242239
/** Is a synthetic function name, i.e. one of
243240
* - 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
@@ -741,6 +741,8 @@ object StdNames {
741741
val XOR : N = "^"
742742
val ZAND : N = "&&"
743743
val ZOR : N = "||"
744+
val PUREARROW: N = "->"
745+
val PURECTXARROW: N = "?->"
744746

745747
// unary operators
746748
val UNARY_PREFIX: N = "unary_"

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ object Types {
183183
case _ => false
184184
}
185185

186-
/** Is this type a (possibly refined or applied or aliased) type reference
186+
/** Is this type a (possibly refined, applied, aliased or annotated) type reference
187187
* to the given type symbol?
188188
* @sym The symbol to compare to. It must be a class symbol or abstract type.
189189
* It makes no sense for it to be an alias type because isRef would always
@@ -204,9 +204,7 @@ object Types {
204204
case this1: TypeVar =>
205205
this1.instanceOpt.isRef(sym, skipRefined)
206206
case this1: AnnotatedType =>
207-
this1 match
208-
case CapturingType(_, _, _) => false
209-
case _ => this1.parent.isRef(sym, skipRefined)
207+
this1.parent.isRef(sym, skipRefined)
210208
case _ => false
211209
}
212210

compiler/src/dotty/tools/dotc/parsing/Parsers.scala

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,47 +1363,56 @@ object Parsers {
13631363
* | InfixType
13641364
* | CaptureSet Type
13651365
* FunType ::= (MonoFunType | PolyFunType)
1366-
* MonoFunType ::= FunTypeArgs (‘=>’ | ‘?=>’) Type
1367-
* PolyFunType ::= HKTypeParamClause '=>' Type
1366+
* MonoFunType ::= FunTypeArgs (‘=>’ | ‘?=>’ | ‘->’ | ‘?->’ ) Type
1367+
* PolyFunType ::= HKTypeParamClause ('=>' | ‘->’_) Type
13681368
* FunTypeArgs ::= InfixType
13691369
* | `(' [ [ ‘[using]’ ‘['erased'] FunArgType {`,' FunArgType } ] `)'
13701370
* | '(' [ ‘[using]’ ‘['erased'] TypedFunParam {',' TypedFunParam } ')'
13711371
* CaptureSet ::= `{` CaptureRef {`,` CaptureRef} `}`
13721372
* CaptureRef ::= Ident
13731373
*/
1374-
def typ(): Tree = {
1374+
def typ(): Tree =
13751375
val start = in.offset
13761376
var imods = Modifiers()
13771377
def functionRest(params: List[Tree]): Tree =
13781378
val paramSpan = Span(start, in.lastOffset)
13791379
atSpan(start, in.offset) {
1380-
if in.token == TLARROW then
1380+
var token = in.token
1381+
if in.isIdent(nme.PUREARROW) then
1382+
token = ARROW
1383+
else if in.isIdent(nme.PURECTXARROW) then
1384+
token = CTXARROW
1385+
else if token == TLARROW then
13811386
if !imods.flags.isEmpty || params.isEmpty then
13821387
syntaxError(em"illegal parameter list for type lambda", start)
1383-
in.token = ARROW
1384-
else
1385-
for case ValDef(_, tpt: ByNameTypeTree, _) <- params do
1386-
syntaxError(em"parameter of type lambda may not be call-by-name", tpt.span)
1387-
in.nextToken()
1388-
return TermLambdaTypeTree(params.asInstanceOf[List[ValDef]], typ())
1388+
token = ARROW
1389+
else if ctx.settings.Ycc.value then
1390+
// `=>` means impure function under -Ycc whereas `->` is a regular function.
1391+
// Without -Ycc they both mean regular function.
1392+
imods |= Impure
13891393

1390-
if in.token == CTXARROW then
1394+
if token == CTXARROW then
13911395
in.nextToken()
13921396
imods |= Given
1397+
else if token == ARROW || token == TLARROW then
1398+
in.nextToken()
13931399
else
13941400
accept(ARROW)
1395-
val t = typ()
13961401

1397-
if imods.isOneOf(Given | Erased) then
1402+
val resultType = typ()
1403+
if token == TLARROW then
1404+
for case ValDef(_, tpt: ByNameTypeTree, _) <- params do
1405+
syntaxError(em"parameter of type lambda may not be call-by-name", tpt.span)
1406+
TermLambdaTypeTree(params.asInstanceOf[List[ValDef]], resultType)
1407+
else if imods.isOneOf(Given | Erased | Impure) then
13981408
if imods.is(Given) && params.isEmpty then
13991409
syntaxError("context function types require at least one parameter", paramSpan)
1400-
new FunctionWithMods(params, t, imods)
1410+
FunctionWithMods(params, resultType, imods)
14011411
else if !ctx.settings.YkindProjector.isDefault then
1402-
val (newParams :+ newT, tparams) = replaceKindProjectorPlaceholders(params :+ t)
1403-
1404-
lambdaAbstract(tparams, Function(newParams, newT))
1412+
val (newParams :+ newResultType, tparams) = replaceKindProjectorPlaceholders(params :+ resultType)
1413+
lambdaAbstract(tparams, Function(newParams, newResultType))
14051414
else
1406-
Function(params, t)
1415+
Function(params, resultType)
14071416
}
14081417
def funTypeArgsRest(first: Tree, following: () => Tree) = {
14091418
val buf = new ListBuffer[Tree] += first
@@ -1461,7 +1470,7 @@ object Parsers {
14611470
val tparams = typeParamClause(ParamOwner.TypeParam)
14621471
if (in.token == TLARROW)
14631472
atSpan(start, in.skipToken())(LambdaTypeTree(tparams, toplevelTyp()))
1464-
else if (in.token == ARROW) {
1473+
else if (in.token == ARROW || in.isIdent(nme.PUREARROW)) {
14651474
val arrowOffset = in.skipToken()
14661475
val body = toplevelTyp()
14671476
atSpan(start, arrowOffset) {
@@ -1482,16 +1491,18 @@ object Parsers {
14821491
else if (in.token == INDENT) enclosed(INDENT, typ())
14831492
else infixType()
14841493

1485-
in.token match {
1494+
in.token match
14861495
case ARROW | CTXARROW => functionRest(t :: Nil)
14871496
case MATCH => matchType(t)
14881497
case FORSOME => syntaxError(ExistentialTypesNoLongerSupported()); t
14891498
case _ =>
1490-
if (imods.is(Erased) && !t.isInstanceOf[FunctionWithMods])
1491-
syntaxError(ErasedTypesCanOnlyBeFunctionTypes(), implicitKwPos(start))
1492-
t
1493-
}
1494-
}
1499+
if isIdent(nme.PUREARROW) || isIdent(nme.PURECTXARROW) then
1500+
functionRest(t :: Nil)
1501+
else
1502+
if (imods.is(Erased) && !t.isInstanceOf[FunctionWithMods])
1503+
syntaxError(ErasedTypesCanOnlyBeFunctionTypes(), implicitKwPos(start))
1504+
t
1505+
end typ
14951506

14961507
private def makeKindProjectorTypeDef(name: TypeName): TypeDef = {
14971508
val isVarianceAnnotated = name.startsWith("+") || name.startsWith("-")
@@ -1549,7 +1560,7 @@ object Parsers {
15491560
def infixTypeRest(t: Tree): Tree =
15501561
infixOps(t, canStartInfixTypeTokens, refinedTypeFn, Location.ElseWhere,
15511562
isType = true,
1552-
isOperator = !followingIsVararg())
1563+
isOperator = !followingIsVararg() && !isIdent(nme.PUREARROW) && !isIdent(nme.PURECTXARROW))
15531564

15541565
/** RefinedType ::= WithType {[nl] Refinement}
15551566
*/

compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,14 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
138138
else simpleNameString(tsym)
139139
}
140140

141-
private def arrow(isGiven: Boolean): String =
142-
if isGiven then "?=>" else "=>"
141+
private def arrow(isGiven: Boolean, isPure: Boolean): String =
142+
(if isGiven then "?" else "") + (if isPure then "->" else "=>")
143143

144144
override def toText(tp: Type): Text = controlled {
145145
def toTextTuple(args: List[Type]): Text =
146146
"(" ~ argsText(args) ~ ")"
147147

148-
def toTextFunction(args: List[Type], isGiven: Boolean, isErased: Boolean): Text =
148+
def toTextFunction(args: List[Type], isGiven: Boolean, isErased: Boolean, isPure: Boolean): Text =
149149
changePrec(GlobalPrec) {
150150
val argStr: Text =
151151
if args.length == 2
@@ -158,26 +158,26 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
158158
~ keywordText("erased ").provided(isErased)
159159
~ argsText(args.init)
160160
~ ")"
161-
argStr ~ " " ~ arrow(isGiven) ~ " " ~ argText(args.last)
161+
argStr ~ " " ~ arrow(isGiven, isPure) ~ " " ~ argText(args.last)
162162
}
163163

164-
def toTextMethodAsFunction(info: Type): Text = info match
164+
def toTextMethodAsFunction(info: Type, isPure: Boolean): Text = info match
165165
case info: MethodType =>
166166
changePrec(GlobalPrec) {
167167
"("
168168
~ keywordText("erased ").provided(info.isErasedMethod)
169169
~ paramsText(info)
170170
~ ") "
171-
~ arrow(info.isImplicitMethod)
171+
~ arrow(info.isImplicitMethod, isPure)
172172
~ " "
173-
~ toTextMethodAsFunction(info.resultType)
173+
~ toTextMethodAsFunction(info.resultType, isPure)
174174
}
175175
case info: PolyType =>
176176
changePrec(GlobalPrec) {
177177
"["
178178
~ paramsText(info)
179179
~ "] => "
180-
~ toTextMethodAsFunction(info.resultType)
180+
~ toTextMethodAsFunction(info.resultType, isPure)
181181
}
182182
case _ =>
183183
toText(info)
@@ -216,9 +216,11 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
216216

217217
def appliedText(tp: Type): Text = tp match
218218
case tp @ AppliedType(tycon, args) =>
219-
val cls = tycon.typeSymbol
219+
val tsym = tycon.typeSymbol
220220
if tycon.isRepeatedParam then toTextLocal(args.head) ~ "*"
221-
else if defn.isFunctionClass(cls) then toTextFunction(args, cls.name.isContextFunction, cls.name.isErasedFunction)
221+
else if defn.isFunctionSymbol(tsym) then
222+
toTextFunction(args, tsym.name.isContextFunction, tsym.name.isErasedFunction,
223+
isPure = ctx.settings.Ycc.value && !tsym.name.isImpureFunction)
222224
else if tp.tupleArity >= 2 && !printDebug then toTextTuple(tp.tupleElementTypes)
223225
else if isInfixType(tp) then
224226
val l :: r :: Nil = args
@@ -245,7 +247,8 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
245247
// don't eta contract if the application would be printed specially
246248
toText(tycon)
247249
case tp: RefinedType if defn.isFunctionOrPolyType(tp) && !printDebug =>
248-
toTextMethodAsFunction(tp.refinedInfo)
250+
toTextMethodAsFunction(tp.refinedInfo,
251+
isPure = ctx.settings.Ycc.value && !tp.typeSymbol.name.isImpureFunction)
249252
case tp: TypeRef =>
250253
if (tp.symbol.isAnonymousClass && !showUniqueIds)
251254
toText(tp.info)
@@ -261,7 +264,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
261264
case tp: ClassInfo =>
262265
if tp.cls.derivesFrom(defn.PolyFunctionClass) then
263266
tp.member(nme.apply).info match
264-
case info: PolyType => return toTextMethodAsFunction(info)
267+
case info: PolyType => return toTextMethodAsFunction(info, isPure = false)
265268
case _ =>
266269
toTextParents(tp.parents) ~~ "{...}"
267270
case JavaArrayType(elemtp) =>
@@ -529,7 +532,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
529532
changePrec(OrTypePrec) { toText(args(0)) ~ " | " ~ atPrec(OrTypePrec + 1) { toText(args(1)) } }
530533
else if (tpt.symbol == defn.andType && args.length == 2)
531534
changePrec(AndTypePrec) { toText(args(0)) ~ " & " ~ atPrec(AndTypePrec + 1) { toText(args(1)) } }
532-
else if defn.isFunctionClass(tpt.symbol)
535+
else if defn.isFunctionSymbol(tpt.symbol)
533536
&& tpt.isInstanceOf[TypeTree] && tree.hasType && !printDebug
534537
then changePrec(GlobalPrec) { toText(tree.typeOpt) }
535538
else args match
@@ -657,7 +660,12 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
657660
~ Text(args.map(argToText), ", ")
658661
~ ")"
659662
}
660-
argsText ~ " " ~ arrow(isGiven) ~ " " ~ toText(body)
663+
val isPure =
664+
ctx.settings.Ycc.value
665+
&& tree.match
666+
case tree: FunctionWithMods => !tree.mods.is(Impure)
667+
case _ => true
668+
argsText ~ " " ~ arrow(isGiven, isPure) ~ " " ~ toText(body)
661669
case PolyFunction(targs, body) =>
662670
val targsText = "[" ~ Text(targs.map((arg: Tree) => toText(arg)), ", ") ~ "]"
663671
changePrec(GlobalPrec) {

compiler/src/dotty/tools/dotc/typer/Typer.scala

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,7 +1238,8 @@ class Typer extends Namer
12381238
val numArgs = args.length
12391239
val isContextual = funFlags.is(Given)
12401240
val isErased = funFlags.is(Erased)
1241-
val funCls = defn.FunctionSymbol(numArgs, isContextual, isErased)
1241+
val isImpure = funFlags.is(Impure)
1242+
val funSym = defn.FunctionSymbol(numArgs, isContextual, isErased, isImpure)
12421243

12431244
/** If `app` is a function type with arguments that are all erased classes,
12441245
* turn it into an erased function type.
@@ -1248,7 +1249,7 @@ class Typer extends Namer
12481249
if !isErased
12491250
&& numArgs > 0
12501251
&& args.indexWhere(!_.tpe.isErasedClass) == numArgs =>
1251-
val tycon1 = TypeTree(defn.FunctionSymbol(numArgs, isContextual, isErased = true).typeRef)
1252+
val tycon1 = TypeTree(defn.FunctionSymbol(numArgs, isContextual, true, isImpure).typeRef)
12521253
.withSpan(tycon.span)
12531254
assignType(cpy.AppliedTypeTree(app)(tycon1, args), tycon1, args)
12541255
case _ =>
@@ -1275,7 +1276,7 @@ class Typer extends Namer
12751276
report.error(i"$mt is an illegal function type because it has inter-parameter dependencies", tree.srcPos)
12761277
val resTpt = TypeTree(mt.nonDependentResultApprox).withSpan(body.span)
12771278
val typeArgs = appDef.termParamss.head.map(_.tpt) :+ resTpt
1278-
val tycon = TypeTree(funCls.typeRef)
1279+
val tycon = TypeTree(funSym.typeRef)
12791280
val core = propagateErased(AppliedTypeTree(tycon, typeArgs))
12801281
RefinedTypeTree(core, List(appDef), ctx.owner.asClass)
12811282
end typedDependent
@@ -1286,7 +1287,7 @@ class Typer extends Namer
12861287
using ctx.fresh.setOwner(newRefinedClassSymbol(tree.span)).setNewScope)
12871288
case _ =>
12881289
propagateErased(
1289-
typed(cpy.AppliedTypeTree(tree)(untpd.TypeTree(funCls.typeRef), args :+ body), pt))
1290+
typed(cpy.AppliedTypeTree(tree)(untpd.TypeTree(funSym.typeRef), args :+ body), pt))
12901291
}
12911292
}
12921293

tests/neg-custom-args/capt-wf.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def test(c: Cap, other: String): Unit =
88
val x2: {other} C = ??? // error: cs is empty
99
val s1 = () => "abc"
1010
val x3: {s1} C = ??? // error: cs is empty
11-
val x3a: () => String = s1
11+
val x3a: () -> String = s1
1212
val s2 = () => if x1 == null then "" else "abc"
1313
val x4: {s2} C = ??? // OK
1414
val x5: {c, c} C = ??? // error: redundant
@@ -26,8 +26,8 @@ def test(c: Cap, other: String): Unit =
2626
val y1: {e1} String = ??? // error cs is empty
2727
val y2: {o1} String = ??? // error cs is empty
2828

29-
lazy val ev: (Int => Boolean) = (n: Int) =>
30-
lazy val od: (Int => Boolean) = (n: Int) =>
29+
lazy val ev: (Int -> Boolean) = (n: Int) =>
30+
lazy val od: (Int -> Boolean) = (n: Int) =>
3131
if n == 1 then true else ev(n - 1)
3232
if n == 0 then true else od(n - 1)
3333
val y3: {ev} String = ??? // error cs is empty

0 commit comments

Comments
 (0)