Skip to content

Commit 24e8ecc

Browse files
authored
New Syntax for Capture Variables and Explicit Capture Polymorphism v3 (#23063)
Closes #22490 Builds on #22725 Supersedes #22902 This syntax for capture variables and members caters to the view that "they are another kind", and most conventions we know from higher-kinded type parameters carry over. A postfix `^` (analogous to `[_]`) indicates that a type parameter/member ranges over capture sets ```scala def sayHi[C^](l: Logger^{C}) = l.log("hello world") trait LList[T, C^]: type D^ >: C // or equivalently >: {C} val node: Ref[T^{D}]^ ``` These variables can be bounded by capture-set literals: ```scala trait Foo: type C^ type D^ >: {logger} <: {logger, async, this.C} type E^ : Monad // error, context bounds are forbidden ``` Just as with higher-kinded type parameters, the kind is "contagious", and we can often avoid the hat if the bounds are concrete capture sets or other capture variables: ```scala trait Foo: type C >: {} <: {cap} // no hat needed type D >: C // dito type E = {x,y,z} // dito type F >: Object <: {cap} // error, ill-kinded ``` Capture variables will be erased if capture checking is turned off. In non-capture-checked code, they are essentially type variables within the interval `>: caps.CapSet <: caps.CapSet`. With capture checking turned on and without explicit bounds, the interval corresponds to `>: caps.CapSet^{} <: caps.CapSet^{caps.cap}`. ## Tasks - [x] Bullet-proof the "can avoid the hat" for binders scheme. - [x] Grammar doc. - [x] Update language reference. - [x] Port new tests. - [x] Clarify when and where to produce attachments. - [x] Forbid nonsense intervals mixing capture sets and types.
2 parents f784625 + eeeeb33 commit 24e8ecc

Some content is hidden

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

68 files changed

+894
-253
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,7 @@ object desugar {
642642
.withMods(mods & (GivenOrImplicit | Erased | hasDefault | Tracked) | Param)
643643
}
644644

645-
/** Desugar type def (not param): Under x.moduliity this can expand
645+
/** Desugar type def (not param): Under x.modularity this can expand
646646
* context bounds, which are expanded to evidence ValDefs. These will
647647
* ultimately map to deferred givens.
648648
*/

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

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ object Trees {
3434

3535
val SyntheticUnit: Property.StickyKey[Unit] = Property.StickyKey()
3636

37+
/** Property key for marking capture-set variables and members */
38+
val CaptureVar: Property.StickyKey[Unit] = Property.StickyKey()
39+
3740
/** Trees take a parameter indicating what the type of their `tpe` field
3841
* is. Two choices: `Type` or `Untyped`.
3942
* Untyped trees have type `Tree[Untyped]`.

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

-8
Original file line numberDiff line numberDiff line change
@@ -533,14 +533,6 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
533533
def makeCapsOf(tp: RefTree)(using Context): Tree =
534534
TypeApply(capsInternalDot(nme.capsOf), tp :: Nil)
535535

536-
// Capture set variable `[C^]` becomes: `[C >: CapSet <: CapSet^{cap}]`
537-
def makeCapsBound()(using Context): TypeBoundsTree =
538-
TypeBoundsTree(
539-
Select(scalaDot(nme.caps), tpnme.CapSet),
540-
makeRetaining(
541-
Select(scalaDot(nme.caps), tpnme.CapSet),
542-
Nil, tpnme.retainsCap))
543-
544536
def makeConstructor(tparams: List[TypeDef], vparamss: List[List[ValDef]], rhs: Tree = EmptyTree)(using Context): DefDef =
545537
DefDef(nme.CONSTRUCTOR, joinParams(tparams, vparamss), TypeTree(), rhs)
546538

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

+8
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ object Mode {
9191
*/
9292
val ImplicitExploration: Mode = newMode(12, "ImplicitExploration")
9393

94+
/** We are currently inside a capture set.
95+
* A term name could be a capture variable, so we need to
96+
* check that it is valid to use as type name.
97+
* Since this mode is only used during annotation typing,
98+
* we can reuse the value of `ImplicitExploration` to save bits.
99+
*/
100+
val InCaptureSet: Mode = ImplicitExploration
101+
94102
/** We are currently unpickling Scala2 info */
95103
val Scala2Unpickling: Mode = newMode(13, "Scala2Unpickling")
96104

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

+95-51
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ object Parsers {
227227
def isNumericLit = numericLitTokens contains in.token
228228
def isTemplateIntro = templateIntroTokens contains in.token
229229
def isDclIntro = dclIntroTokens contains in.token
230+
def isDclIntroNext = dclIntroTokens contains in.lookahead.token
230231
def isStatSeqEnd = in.isNestedEnd || in.token == EOF || in.token == RPAREN
231232
def mustStartStat = mustStartStatTokens contains in.token
232233

@@ -1591,8 +1592,7 @@ object Parsers {
15911592
case _ => None
15921593
}
15931594

1594-
/** CaptureRef ::= { SimpleRef `.` } SimpleRef [`*`] [`.` rd]
1595-
* | [ { SimpleRef `.` } SimpleRef `.` ] id `^`
1595+
/** CaptureRef ::= { SimpleRef `.` } SimpleRef [`*`] [`.` `rd`] -- under captureChecking
15961596
*/
15971597
def captureRef(): Tree =
15981598

@@ -1611,22 +1611,17 @@ object Parsers {
16111611
in.nextToken()
16121612
derived(reachRef, nme.CC_READONLY)
16131613
else reachRef
1614-
else if isIdent(nme.UPARROW) then
1615-
in.nextToken()
1616-
atSpan(startOffset(ref)):
1617-
convertToTypeId(ref) match
1618-
case ref: RefTree => makeCapsOf(ref)
1619-
case ref => ref
16201614
else ref
16211615

16221616
recur(simpleRef())
16231617
end captureRef
16241618

16251619
/** CaptureSet ::= `{` CaptureRef {`,` CaptureRef} `}` -- under captureChecking
16261620
*/
1627-
def captureSet(): List[Tree] = inBraces {
1628-
if in.token == RBRACE then Nil else commaSeparated(captureRef)
1629-
}
1621+
def captureSet(): List[Tree] =
1622+
inBraces {
1623+
if in.token == RBRACE then Nil else commaSeparated(captureRef)
1624+
}
16301625

16311626
def capturesAndResult(core: () => Tree): Tree =
16321627
if Feature.ccEnabled && in.token == LBRACE && canStartCaptureSetContentsTokens.contains(in.lookahead.token)
@@ -1640,9 +1635,9 @@ object Parsers {
16401635
* | InfixType
16411636
* FunType ::= (MonoFunType | PolyFunType)
16421637
* MonoFunType ::= FunTypeArgs (‘=>’ | ‘?=>’) Type
1643-
* | (‘->’ | ‘?->’ ) [CaptureSet] Type -- under pureFunctions
1638+
* | (‘->’ | ‘?->’ ) [CaptureSet] Type -- under pureFunctions and captureChecking
16441639
* PolyFunType ::= TypTypeParamClause '=>' Type
1645-
* | TypTypeParamClause ‘->’ [CaptureSet] Type -- under pureFunctions
1640+
* | TypTypeParamClause ‘->’ [CaptureSet] Type -- under pureFunctions and captureChecking
16461641
* FunTypeArgs ::= InfixType
16471642
* | `(' [ FunArgType {`,' FunArgType } ] `)'
16481643
* | '(' [ TypedFunParam {',' TypedFunParam } ')'
@@ -1885,7 +1880,7 @@ object Parsers {
18851880
if in.token == LPAREN then funParamClause() :: funParamClauses() else Nil
18861881

18871882
/** InfixType ::= RefinedType {id [nl] RefinedType}
1888-
* | RefinedType `^` // under capture checking
1883+
* | RefinedType `^` -- under captureChecking
18891884
*/
18901885
def infixType(inContextBound: Boolean = false): Tree = infixTypeRest(inContextBound)(refinedType())
18911886

@@ -1916,6 +1911,12 @@ object Parsers {
19161911
|| !canStartInfixTypeTokens.contains(ahead.token)
19171912
|| ahead.lineOffset > 0
19181913

1914+
inline def gobbleHat(): Boolean =
1915+
if Feature.ccEnabled && isIdent(nme.UPARROW) then
1916+
in.nextToken()
1917+
true
1918+
else false
1919+
19191920
def refinedTypeRest(t: Tree): Tree = {
19201921
argumentStart()
19211922
if in.isNestedStart then
@@ -2172,35 +2173,45 @@ object Parsers {
21722173
atSpan(startOffset(t), startOffset(id)) { Select(t, id.name) }
21732174
}
21742175

2175-
/** ArgTypes ::= Type {`,' Type}
2176-
* | NamedTypeArg {`,' NamedTypeArg}
2177-
* NamedTypeArg ::= id `=' Type
2176+
/** ArgTypes ::= TypeArg {‘,’ TypeArg}
2177+
* | NamedTypeArg {‘,’ NamedTypeArg}
2178+
* TypeArg ::= Type
2179+
* | CaptureSet -- under captureChecking
2180+
* NamedTypeArg ::= id ‘=’ TypeArg
21782181
* NamesAndTypes ::= NameAndType {‘,’ NameAndType}
2179-
* NameAndType ::= id ':' Type
2182+
* NameAndType ::= id ‘:’ Type
21802183
*/
21812184
def argTypes(namedOK: Boolean, wildOK: Boolean, tupleOK: Boolean): List[Tree] =
2182-
def argType() =
2183-
val t = typ()
2185+
def wildCardCheck(gen: Tree): Tree =
2186+
val t = gen
21842187
if wildOK then t else rejectWildcardType(t)
21852188

2186-
def namedArgType() =
2189+
def argType() = wildCardCheck(typ())
2190+
2191+
def typeArg() = wildCardCheck:
2192+
if Feature.ccEnabled && in.token == LBRACE && !isDclIntroNext then // is this a capture set and not a refinement type?
2193+
// This case is ambiguous w.r.t. an Object literal {}. But since CC is enabled, we probably expect it to designate the empty set
2194+
concreteCapsType(captureSet())
2195+
else typ()
2196+
2197+
def namedTypeArg() =
21872198
atSpan(in.offset):
21882199
val name = ident()
21892200
accept(EQUALS)
2190-
NamedArg(name.toTypeName, argType())
2201+
NamedArg(name.toTypeName, typeArg())
21912202

2192-
def namedElem() =
2203+
def nameAndType() =
21932204
atSpan(in.offset):
21942205
val name = ident()
21952206
acceptColon()
21962207
NamedArg(name, argType())
21972208

2198-
if namedOK && isIdent && in.lookahead.token == EQUALS then
2199-
commaSeparated(() => namedArgType())
2209+
if namedOK && (isIdent && in.lookahead.token == EQUALS) then
2210+
commaSeparated(() => namedTypeArg())
22002211
else if tupleOK && isIdent && in.lookahead.isColon && sourceVersion.enablesNamedTuples then
2201-
commaSeparated(() => namedElem())
2212+
commaSeparated(() => nameAndType())
22022213
else
2203-
commaSeparated(() => argType())
2214+
commaSeparated(() => typeArg())
22042215
end argTypes
22052216

22062217
def paramTypeOf(core: () => Tree): Tree =
@@ -2244,7 +2255,7 @@ object Parsers {
22442255
PostfixOp(t, Ident(tpnme.raw.STAR))
22452256
else t
22462257

2247-
/** TypeArgs ::= `[' Type {`,' Type} `]'
2258+
/** TypeArgs ::= `[' TypeArg {`,' TypeArg} `]'
22482259
* NamedTypeArgs ::= `[' NamedTypeArg {`,' NamedTypeArg} `]'
22492260
*/
22502261
def typeArgs(namedOK: Boolean, wildOK: Boolean): List[Tree] =
@@ -2258,21 +2269,28 @@ object Parsers {
22582269
else
22592270
inBraces(refineStatSeq())
22602271

2261-
/** TypeBounds ::= [`>:' Type] [`<:' Type]
2262-
* | `^` -- under captureChecking
2272+
/** TypeBounds ::= [`>:' TypeBound ] [`<:' TypeBound ]
2273+
* TypeBound ::= Type
2274+
* | CaptureSet -- under captureChecking
22632275
*/
22642276
def typeBounds(): TypeBoundsTree =
22652277
atSpan(in.offset):
2266-
if in.isIdent(nme.UPARROW) && Feature.ccEnabled then
2267-
in.nextToken()
2268-
makeCapsBound()
2269-
else
2270-
TypeBoundsTree(bound(SUPERTYPE), bound(SUBTYPE))
2278+
TypeBoundsTree(bound(SUPERTYPE), bound(SUBTYPE))
22712279

22722280
private def bound(tok: Int): Tree =
2273-
if (in.token == tok) { in.nextToken(); toplevelTyp() }
2281+
if in.token == tok then
2282+
in.nextToken()
2283+
if Feature.ccEnabled && in.token == LBRACE && !isDclIntroNext then
2284+
capsBound(captureSet(), isLowerBound = tok == SUPERTYPE)
2285+
else toplevelTyp()
22742286
else EmptyTree
22752287

2288+
private def capsBound(refs: List[Tree], isLowerBound: Boolean = false): Tree =
2289+
if isLowerBound && refs.isEmpty then // lower bounds with empty capture sets become a pure CapSet
2290+
Select(scalaDot(nme.caps), tpnme.CapSet)
2291+
else
2292+
concreteCapsType(refs)
2293+
22762294
/** TypeAndCtxBounds ::= TypeBounds [`:` ContextBounds]
22772295
*/
22782296
def typeAndCtxBounds(pname: TypeName): Tree = {
@@ -3397,7 +3415,7 @@ object Parsers {
33973415
* | opaque
33983416
* LocalModifier ::= abstract | final | sealed | open | implicit | lazy | erased |
33993417
* inline | transparent | infix |
3400-
* mut -- under cc
3418+
* mut -- under captureChecking
34013419
*/
34023420
def modifiers(allowed: BitSet = modifierTokens, start: Modifiers = Modifiers()): Modifiers = {
34033421
@tailrec
@@ -3486,22 +3504,25 @@ object Parsers {
34863504
recur(numLeadParams, firstClause = true, prevIsTypeClause = false)
34873505
end typeOrTermParamClauses
34883506

3489-
34903507
/** ClsTypeParamClause::= ‘[’ ClsTypeParam {‘,’ ClsTypeParam} ‘]’
3491-
* ClsTypeParam ::= {Annotation} [‘+’ | ‘-’]
3492-
* id [HkTypeParamClause] TypeAndCtxBounds
3508+
* ClsTypeParam ::= {Annotation} [‘+’ | ‘-’]
3509+
* id [HkTypeParamClause] TypeAndCtxBounds
3510+
* | {Annotation} [‘+’ | ‘-’] id `^` TypeAndCtxBounds -- under captureChecking
34933511
*
34943512
* DefTypeParamClause::= ‘[’ DefTypeParam {‘,’ DefTypeParam} ‘]’
3495-
* DefTypeParam ::= {Annotation}
3496-
* id [HkTypeParamClause] TypeAndCtxBounds
3513+
* DefTypeParam ::= {Annotation}
3514+
* id [HkTypeParamClause] TypeAndCtxBounds
3515+
* | {Annotation} id `^` TypeAndCtxBounds -- under captureChecking
34973516
*
34983517
* TypTypeParamClause::= ‘[’ TypTypeParam {‘,’ TypTypeParam} ‘]’
3499-
* TypTypeParam ::= {Annotation}
3500-
* (id | ‘_’) [HkTypeParamClause] TypeAndCtxBounds
3518+
* TypTypeParam ::= {Annotation}
3519+
* (id | ‘_’) [HkTypeParamClause] TypeAndCtxBounds
3520+
* | {Annotation} (id | ‘_’) `^` TypeAndCtxBounds -- under captureChecking
35013521
*
35023522
* HkTypeParamClause ::= ‘[’ HkTypeParam {‘,’ HkTypeParam} ‘]’
3503-
* HkTypeParam ::= {Annotation} [‘+’ | ‘-’]
3504-
* (id | ‘_’) [HkTypeParamClause] TypeBounds
3523+
* HkTypeParam ::= {Annotation} [‘+’ | ‘-’]
3524+
* (id | ‘_’) [HkTypePamClause] TypeBounds
3525+
* | {Annotation} [‘+’ | ‘-’] (id | ‘_’) `^` TypeBounds -- under captureChecking
35053526
*/
35063527
def typeParamClause(paramOwner: ParamOwner): List[TypeDef] = inBracketsWithCommas {
35073528

@@ -3526,12 +3547,18 @@ object Parsers {
35263547
in.nextToken()
35273548
WildcardParamName.fresh().toTypeName
35283549
else ident().toTypeName
3550+
val isCap = gobbleHat()
35293551
val hkparams = typeParamClauseOpt(ParamOwner.Hk)
35303552
val bounds =
35313553
if paramOwner.acceptsCtxBounds then typeAndCtxBounds(name)
35323554
else if sourceVersion.enablesNewGivens && paramOwner == ParamOwner.Type then typeAndCtxBounds(name)
35333555
else typeBounds()
3534-
TypeDef(name, lambdaAbstract(hkparams, bounds)).withMods(mods)
3556+
val res = TypeDef(name, lambdaAbstract(hkparams, bounds)).withMods(mods)
3557+
if isCap then
3558+
res.pushAttachment(CaptureVar, ())
3559+
// putting the attachment here as well makes post-processing in the typer easier
3560+
bounds.pushAttachment(CaptureVar, ())
3561+
res
35353562
}
35363563
}
35373564
commaSeparated(() => typeParam())
@@ -4052,33 +4079,47 @@ object Parsers {
40524079
argumentExprss(mkApply(Ident(nme.CONSTRUCTOR), argumentExprs()))
40534080
}
40544081

4055-
/** TypeDef ::= id [HkTypeParamClause] {FunParamClause} TypeAndCtxBounds [‘=’ Type]
4082+
/** TypeDef ::= id [HkTypeParamClause] {FunParamClause} TypeAndCtxBounds [‘=’ TypeDefRHS ]
4083+
* | id `^` TypeAndCtxBounds [‘=’ TypeDefRHS ] -- under captureChecking
4084+
* TypeDefRHS ::= Type
4085+
* | CaptureSet -- under captureChecking
40564086
*/
40574087
def typeDefOrDcl(start: Offset, mods: Modifiers): Tree = {
4088+
4089+
def typeDefRHS(): Tree =
4090+
if Feature.ccEnabled && in.token == LBRACE && !isDclIntroNext then
4091+
concreteCapsType(captureSet())
4092+
else toplevelTyp()
4093+
40584094
newLinesOpt()
40594095
atSpan(start, nameStart) {
40604096
val nameIdent = typeIdent()
4097+
val isCapDef = gobbleHat()
40614098
val tname = nameIdent.name.asTypeName
40624099
val tparams = typeParamClauseOpt(ParamOwner.Hk)
40634100
val vparamss = funParamClauses()
40644101

40654102
def makeTypeDef(rhs: Tree): Tree = {
40664103
val rhs1 = lambdaAbstractAll(tparams :: vparamss, rhs)
40674104
val tdef = TypeDef(nameIdent.name.toTypeName, rhs1)
4068-
if (nameIdent.isBackquoted)
4105+
if nameIdent.isBackquoted then
40694106
tdef.pushAttachment(Backquoted, ())
4107+
if isCapDef then
4108+
tdef.pushAttachment(CaptureVar, ())
4109+
// putting the attachment here as well makes post-processing in the typer easier
4110+
rhs.pushAttachment(CaptureVar, ())
40704111
finalizeDef(tdef, mods, start)
40714112
}
40724113

40734114
in.token match {
40744115
case EQUALS =>
40754116
in.nextToken()
4076-
makeTypeDef(toplevelTyp())
4117+
makeTypeDef(typeDefRHS())
40774118
case SUBTYPE | SUPERTYPE =>
40784119
typeAndCtxBounds(tname) match
40794120
case bounds: TypeBoundsTree if in.token == EQUALS =>
40804121
val eqOffset = in.skipToken()
4081-
var rhs = toplevelTyp()
4122+
var rhs = typeDefRHS()
40824123
rhs match {
40834124
case mtt: MatchTypeTree =>
40844125
bounds match {
@@ -4107,6 +4148,9 @@ object Parsers {
41074148
}
41084149
}
41094150

4151+
private def concreteCapsType(refs: List[Tree]): Tree =
4152+
makeRetaining(Select(scalaDot(nme.caps), tpnme.CapSet), refs, tpnme.retains)
4153+
41104154
/** TmplDef ::= ([‘case’] ‘class’ | ‘trait’) ClassDef
41114155
* | [‘case’] ‘object’ ObjectDef
41124156
* | ‘enum’ EnumDef

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ class PlainPrinter(_ctx: Context) extends Printer {
451451
homogenize(tp) match
452452
case tp: TermRef if tp.symbol == defn.captureRoot => "cap"
453453
case tp: SingletonType => toTextRef(tp)
454-
case tp: (TypeRef | TypeParamRef) => toText(tp) ~ "^"
454+
case tp: (TypeRef | TypeParamRef) => toText(tp)
455455
case ReadOnlyCapability(tp1) => toTextCaptureRef(tp1) ~ ".rd"
456456
case ReachCapability(tp1) => toTextCaptureRef(tp1) ~ "*"
457457
case MaybeCapability(tp1) => toTextCaptureRef(tp1) ~ "?"

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import annotation.threadUnsafe
3737
import scala.util.control.NonFatal
3838
import dotty.tools.dotc.inlines.Inlines
3939
import scala.annotation.tailrec
40+
import dotty.tools.dotc.cc.isRetains
4041

4142
object Applications {
4243
import tpd.*
@@ -1116,7 +1117,9 @@ trait Applications extends Compatibility {
11161117
val fun2 = Applications.retypeSignaturePolymorphicFn(fun1, methType)
11171118
simpleApply(fun2, proto)
11181119
case funRef: TermRef =>
1119-
val app = ApplyTo(tree, fun1, funRef, proto, pt)
1120+
// println(i"typedApply: $funRef, ${tree.args}, ${funRef.symbol.maybeOwner.isRetains}")
1121+
val applyCtx = if funRef.symbol.maybeOwner.isRetains then ctx.addMode(Mode.InCaptureSet) else ctx
1122+
val app = ApplyTo(tree, fun1, funRef, proto, pt)(using applyCtx)
11201123
convertNewGenericArray(
11211124
widenEnumCase(
11221125
postProcessByNameArgs(funRef, app).computeNullable(),

0 commit comments

Comments
 (0)