Skip to content

New Syntax for Capture Variables and Explicit Capture Polymorphism v3 #23063

New issue

Have a question about this project? No Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “No Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? No Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions compiler/src/dotty/tools/dotc/ast/untpd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -534,12 +534,13 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
TypeApply(capsInternalDot(nme.capsOf), tp :: Nil)

// Capture set variable `[C^]` becomes: `[C >: CapSet <: CapSet^{cap}]`
def makeCapsBound()(using Context): TypeBoundsTree =
TypeBoundsTree(
Select(scalaDot(nme.caps), tpnme.CapSet),
makeRetaining(
Select(scalaDot(nme.caps), tpnme.CapSet),
Nil, tpnme.retainsCap))
def makeCapsBound(refsL: List[Tree] = Nil, refsU: List[Tree] = Nil)(using Context): TypeBoundsTree =
val lower = refsL match
case Nil => Select(scalaDot(nme.caps), tpnme.CapSet)
case refsL => makeRetaining(Select(scalaDot(nme.caps), tpnme.CapSet), refsL, tpnme.retains)
val upper =
makeRetaining(Select(scalaDot(nme.caps), tpnme.CapSet), refsU, if refsU.isEmpty then tpnme.retainsCap else tpnme.retains)
TypeBoundsTree(lower, upper)

def makeConstructor(tparams: List[TypeDef], vparamss: List[List[ValDef]], rhs: Tree = EmptyTree)(using Context): DefDef =
DefDef(nme.CONSTRUCTOR, joinParams(tparams, vparamss), TypeTree(), rhs)
Expand Down
8 changes: 8 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Mode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ object Mode {
*/
val ImplicitExploration: Mode = newMode(12, "ImplicitExploration")

/** We are currently inside a capture set.
* A term name could be a capture variable, so we need to
* check that it is valid to use as type name.
* Since this mode is only used during annotation typing,
* we can reuse the value of `ImplicitExploration` to save bits.
*/
val InCaptureSet: Mode = ImplicitExploration

/** We are currently unpickling Scala2 info */
val Scala2Unpickling: Mode = newMode(13, "Scala2Unpickling")

Expand Down
171 changes: 109 additions & 62 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ object Parsers {
def isNumericLit = numericLitTokens contains in.token
def isTemplateIntro = templateIntroTokens contains in.token
def isDclIntro = dclIntroTokens contains in.token
def isDclIntroNext = dclIntroTokens contains in.lookahead.token
def isStatSeqEnd = in.isNestedEnd || in.token == EOF || in.token == RPAREN
def mustStartStat = mustStartStatTokens contains in.token

Expand Down Expand Up @@ -1601,8 +1602,8 @@ object Parsers {
case _ => None
}

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

Expand All @@ -1621,22 +1622,20 @@ object Parsers {
in.nextToken()
derived(reachRef, nme.CC_READONLY)
else reachRef
else if isIdent(nme.UPARROW) then
in.nextToken()
atSpan(startOffset(ref)):
convertToTypeId(ref) match
case ref: RefTree => makeCapsOf(ref)
case ref => ref
else ref

recur(simpleRef())
end captureRef

/** CaptureSet ::= `{` CaptureRef {`,` CaptureRef} `}` -- under captureChecking
*/
def captureSet(): List[Tree] = inBraces {
if in.token == RBRACE then Nil else commaSeparated(captureRef)
}
def captureSet(): List[Tree] =
if in.token != LBRACE then
syntaxError(em"expected '{' to start capture set", in.offset)
Nil
else inBraces {
if in.token == RBRACE then Nil else commaSeparated(captureRef)
}

def capturesAndResult(core: () => Tree): Tree =
if Feature.ccEnabled && in.token == LBRACE && canStartCaptureSetContentsTokens.contains(in.lookahead.token)
Expand All @@ -1650,9 +1649,9 @@ object Parsers {
* | InfixType
* FunType ::= (MonoFunType | PolyFunType)
* MonoFunType ::= FunTypeArgs (‘=>’ | ‘?=>’) Type
* | (‘->’ | ‘?->’ ) [CaptureSet] Type -- under pureFunctions
* | (‘->’ | ‘?->’ ) [CaptureSet] Type -- under pureFunctions and captureChecking
* PolyFunType ::= TypTypeParamClause '=>' Type
* | TypTypeParamClause ‘->’ [CaptureSet] Type -- under pureFunctions
* | TypTypeParamClause ‘->’ [CaptureSet] Type -- under pureFunctions and captureChecking
* FunTypeArgs ::= InfixType
* | `(' [ FunArgType {`,' FunArgType } ] `)'
* | '(' [ TypedFunParam {',' TypedFunParam } ')'
Expand Down Expand Up @@ -1895,7 +1894,7 @@ object Parsers {
if in.token == LPAREN then funParamClause() :: funParamClauses() else Nil

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

Expand Down Expand Up @@ -1926,6 +1925,12 @@ object Parsers {
|| !canStartInfixTypeTokens.contains(ahead.token)
|| ahead.lineOffset > 0

inline def gobbleHat(): Boolean =
if Feature.ccEnabled && isIdent(nme.UPARROW) then
in.nextToken()
true
else false

def refinedTypeRest(t: Tree): Tree = {
argumentStart()
if in.isNestedStart then
Expand Down Expand Up @@ -2182,35 +2187,45 @@ object Parsers {
atSpan(startOffset(t), startOffset(id)) { Select(t, id.name) }
}

/** ArgTypes ::= Type {`,' Type}
* | NamedTypeArg {`,' NamedTypeArg}
* NamedTypeArg ::= id `=' Type
/** ArgTypes ::= TypeArg {‘,’ TypeArg}
* | NamedTypeArg {‘,’ NamedTypeArg}
* TypeArg ::= Type
* | CaptureSet -- under captureChecking
* NamedTypeArg ::= id ‘=’ TypeArg
* NamesAndTypes ::= NameAndType {‘,’ NameAndType}
* NameAndType ::= id ':' Type
* NameAndType ::= id ‘:’ Type
*/
def argTypes(namedOK: Boolean, wildOK: Boolean, tupleOK: Boolean): List[Tree] =
def argType() =
val t = typ()
inline def wildCardCheck(inline gen: Tree): Tree =
val t = gen
if wildOK then t else rejectWildcardType(t)

def namedArgType() =
def argType() = wildCardCheck(typ())

def typeArg() = wildCardCheck:
if Feature.ccEnabled && in.token == LBRACE && !isDclIntroNext then // is this a capture set and not a refinement type?
// This case is ambiguous w.r.t. an Object literal {}. But since CC is enabled, we probably expect it to designate the empty set
concreteCapsType(captureSet())
else typ()

def namedTypeArg() =
atSpan(in.offset):
val name = ident()
accept(EQUALS)
NamedArg(name.toTypeName, argType())
NamedArg(name.toTypeName, typeArg())

def namedElem() =
def nameAndType() =
atSpan(in.offset):
val name = ident()
acceptColon()
NamedArg(name, argType())

if namedOK && isIdent && in.lookahead.token == EQUALS then
commaSeparated(() => namedArgType())
if namedOK && (isIdent && in.lookahead.token == EQUALS) then
commaSeparated(() => namedTypeArg())
else if tupleOK && isIdent && in.lookahead.isColon && sourceVersion.enablesNamedTuples then
commaSeparated(() => namedElem())
commaSeparated(() => nameAndType()) // TODO: can capture-set variables occur here?
else
commaSeparated(() => argType())
commaSeparated(() => typeArg())
end argTypes

def paramTypeOf(core: () => Tree): Tree =
Expand Down Expand Up @@ -2254,7 +2269,7 @@ object Parsers {
PostfixOp(t, Ident(tpnme.raw.STAR))
else t

/** TypeArgs ::= `[' Type {`,' Type} `]'
/** TypeArgs ::= `[' TypeArg {`,' TypeArg} `]'
* NamedTypeArgs ::= `[' NamedTypeArg {`,' NamedTypeArg} `]'
*/
def typeArgs(namedOK: Boolean, wildOK: Boolean): List[Tree] =
Expand All @@ -2268,25 +2283,34 @@ object Parsers {
else
inBraces(refineStatSeq())

/** TypeBounds ::= [`>:' Type] [`<:' Type]
* | `^` -- under captureChecking
/** TypeBounds ::= [`>:' TypeBound ] [`<:' TypeBound ]
* TypeBound ::= Type
* | CaptureSet -- under captureChecking
*/
def typeBounds(): TypeBoundsTree =
def typeBounds(isCapParamOrMem: Boolean = false): TypeBoundsTree =
atSpan(in.offset):
if in.isIdent(nme.UPARROW) && Feature.ccEnabled then
in.nextToken()
makeCapsBound()
else
TypeBoundsTree(bound(SUPERTYPE), bound(SUBTYPE))
TypeBoundsTree(bound(SUPERTYPE, isCapParamOrMem), bound(SUBTYPE, isCapParamOrMem))

private def bound(tok: Int): Tree =
if (in.token == tok) { in.nextToken(); toplevelTyp() }
private def bound(tok: Int, isCapParamOrMem: Boolean = false): Tree =
if (in.token == tok) then
in.nextToken()
if Feature.ccEnabled && (in.token == LBRACE && !isDclIntroNext) then
capsBound(captureSet(), isLowerBound = tok == SUPERTYPE)
else toplevelTyp()
else if Feature.ccEnabled && isCapParamOrMem then
capsBound(Nil, isLowerBound = tok == SUPERTYPE) // FIXME: should we avoid the CapSet^{} lower bound and make it Nothing?
else EmptyTree

private def capsBound(refs: List[Tree], isLowerBound: Boolean = false): Tree =
if isLowerBound && refs.isEmpty then // lower bounds with empty capture sets become a pure CapSet
Select(scalaDot(nme.caps), tpnme.CapSet)
else
makeRetaining(Select(scalaDot(nme.caps), tpnme.CapSet), refs, if refs.isEmpty then tpnme.retainsCap else tpnme.retains)

/** TypeAndCtxBounds ::= TypeBounds [`:` ContextBounds]
*/
def typeAndCtxBounds(pname: TypeName): Tree = {
val t = typeBounds()
def typeAndCtxBounds(pname: TypeName, isCapParamOrMem: Boolean = false): Tree = {
val t = typeBounds(isCapParamOrMem)
val cbs = contextBounds(pname)
if (cbs.isEmpty) t
else atSpan((t.span union cbs.head.span).start) { ContextBounds(t, cbs) }
Expand Down Expand Up @@ -3403,7 +3427,7 @@ object Parsers {
* | opaque
* LocalModifier ::= abstract | final | sealed | open | implicit | lazy | erased |
* inline | transparent | infix |
* mut -- under cc
* mut -- under captureChecking
*/
def modifiers(allowed: BitSet = modifierTokens, start: Modifiers = Modifiers()): Modifiers = {
@tailrec
Expand Down Expand Up @@ -3492,22 +3516,25 @@ object Parsers {
recur(numLeadParams, firstClause = true, prevIsTypeClause = false)
end typeOrTermParamClauses


/** ClsTypeParamClause::= ‘[’ ClsTypeParam {‘,’ ClsTypeParam} ‘]’
* ClsTypeParam ::= {Annotation} [‘+’ | ‘-’]
* id [HkTypeParamClause] TypeAndCtxBounds
* ClsTypeParam ::= {Annotation} [‘+’ | ‘-’]
* id [HkTypeParamClause] TypeAndCtxBounds
* | {Annotation} id [`^`] TypeAndCtxBounds -- under captureChecking
*
* DefTypeParamClause::= ‘[’ DefTypeParam {‘,’ DefTypeParam} ‘]’
* DefTypeParam ::= {Annotation}
* id [HkTypeParamClause] TypeAndCtxBounds
* DefTypeParam ::= {Annotation}
* id [HkTypeParamClause] TypeAndCtxBounds
* | {Annotation} id [`^`] TypeAndCtxBounds -- under captureChecking
*
* TypTypeParamClause::= ‘[’ TypTypeParam {‘,’ TypTypeParam} ‘]’
* TypTypeParam ::= {Annotation}
* (id | ‘_’) [HkTypeParamClause] TypeAndCtxBounds
* TypTypeParam ::= {Annotation}
* (id | ‘_’) [HkTypeParamClause] TypeAndCtxBounds
* | {Annotation} (id | ‘_’) [`^`] TypeAndCtxBounds -- under captureChecking
*
* HkTypeParamClause ::= ‘[’ HkTypeParam {‘,’ HkTypeParam} ‘]’
* HkTypeParam ::= {Annotation} [‘+’ | ‘-’]
* (id | ‘_’) [HkTypeParamClause] TypeBounds
* HkTypeParam ::= {Annotation} [‘+’ | ‘-’]
* (id | ‘_’) [HkTypePamClause] TypeBounds
* | {Annotation} (id | ‘_’) [`^`] TypeBounds -- under captureChecking
*/
def typeParamClause(paramOwner: ParamOwner): List[TypeDef] = inBracketsWithCommas {

Expand All @@ -3532,11 +3559,17 @@ object Parsers {
in.nextToken()
WildcardParamName.fresh().toTypeName
else ident().toTypeName
val isCap = gobbleHat()
if isCap && mods.isOneOf(Covariant | Contravariant) then
syntaxError(em"capture parameters cannot have `+/-` variance annotations") // TODO we might want to allow those
if isCap && in.token == LBRACKET then
syntaxError(em"capture parameters do not take type parameters")
in.nextToken()
val hkparams = typeParamClauseOpt(ParamOwner.Hk)
val bounds =
if paramOwner.acceptsCtxBounds then typeAndCtxBounds(name)
else if sourceVersion.enablesNewGivens && paramOwner == ParamOwner.Type then typeAndCtxBounds(name)
else typeBounds()
if paramOwner.acceptsCtxBounds then typeAndCtxBounds(name, isCap)
else if sourceVersion.enablesNewGivens && paramOwner == ParamOwner.Type then typeAndCtxBounds(name, isCap)
else typeBounds(isCap)
TypeDef(name, lambdaAbstract(hkparams, bounds)).withMods(mods)
}
}
Expand Down Expand Up @@ -4058,15 +4091,26 @@ object Parsers {
argumentExprss(mkApply(Ident(nme.CONSTRUCTOR), argumentExprs()))
}

/** TypeDef ::= id [HkTypeParamClause] {FunParamClause} TypeAndCtxBounds [‘=’ Type]
/** TypeDef ::= id [HkTypeParamClause] {FunParamClause} TypeAndCtxBounds [‘=’ TypeDefRHS ]
* | id [`^`] TypeAndCtxBounds [‘=’ TypeDefRHS ] -- under captureChecking
* TypeDefRHS ::= Type
* | CaptureSet -- under captureChecking
*/
def typeDefOrDcl(start: Offset, mods: Modifiers): Tree = {
def typeDefOrDcl(start: Offset, mods: Modifiers): Tree = { // FIXME: ^-qualified members should automatically receive the CapSet interval!

def typeDefRHS(): Tree =
if Feature.ccEnabled && in.token == LBRACE && !isDclIntroNext then
concreteCapsType(captureSet())
else toplevelTyp()

newLinesOpt()
atSpan(start, nameStart) {
val nameIdent = typeIdent()
val isCapDef = gobbleHat()
if isCapDef && in.token == LBRACKET then syntaxError(em"capture-set member declarations cannot have type parameters")
val tname = nameIdent.name.asTypeName
val tparams = typeParamClauseOpt(ParamOwner.Hk)
val vparamss = funParamClauses()
val tparams = if !isCapDef then typeParamClauseOpt(ParamOwner.Hk) else Nil
val vparamss = if !isCapDef then funParamClauses() else Nil

def makeTypeDef(rhs: Tree): Tree = {
val rhs1 = lambdaAbstractAll(tparams :: vparamss, rhs)
Expand All @@ -4079,12 +4123,12 @@ object Parsers {
in.token match {
case EQUALS =>
in.nextToken()
makeTypeDef(toplevelTyp())
makeTypeDef(typeDefRHS())
case SUBTYPE | SUPERTYPE =>
typeAndCtxBounds(tname) match
typeAndCtxBounds(tname, isCapDef) match
case bounds: TypeBoundsTree if in.token == EQUALS =>
val eqOffset = in.skipToken()
var rhs = toplevelTyp()
var rhs = typeDefRHS()
rhs match {
case mtt: MatchTypeTree =>
bounds match {
Expand All @@ -4102,17 +4146,20 @@ object Parsers {
makeTypeDef(rhs)
case bounds => makeTypeDef(bounds)
case SEMI | NEWLINE | NEWLINES | COMMA | RBRACE | OUTDENT | EOF =>
makeTypeDef(typeAndCtxBounds(tname))
makeTypeDef(typeAndCtxBounds(tname, isCapDef))
case _ if (staged & StageKind.QuotedPattern) != 0
|| sourceVersion.enablesNewGivens && in.isColon =>
makeTypeDef(typeAndCtxBounds(tname))
makeTypeDef(typeAndCtxBounds(tname, isCapDef))
case _ =>
syntaxErrorOrIncomplete(ExpectedTypeBoundOrEquals(in.token))
return EmptyTree // return to avoid setting the span to EmptyTree
}
}
}

private def concreteCapsType(refs: List[Tree]): Tree =
makeRetaining(Select(scalaDot(nme.caps), tpnme.CapSet), refs, tpnme.retains)

/** TmplDef ::= ([‘case’] ‘class’ | ‘trait’) ClassDef
* | [‘case’] ‘object’ ObjectDef
* | ‘enum’ EnumDef
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe
case FormatInterpolationErrorID // errorNumber: 209
case ValueClassCannotExtendAliasOfAnyValID // errorNumber: 210
case MatchIsNotPartialFunctionID // errorNumber: 211
case ExpectedCaptureBoundOrEqualsID // errorNumber: 212

def errorNumber = ordinal - 1

Expand Down
17 changes: 17 additions & 0 deletions compiler/src/dotty/tools/dotc/reporting/messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1923,6 +1923,23 @@ class ExpectedTypeBoundOrEquals(found: Token)(using Context)
|"""
}

class ExpectedCaptureBoundOrEquals(found: Token)(using Context)
extends SyntaxMsg(ExpectedCaptureBoundOrEqualsID) {
def msg(using Context) = i"${hl("=")}, ${hl(">:")}, or ${hl("<:")} expected, but ${Tokens.showToken(found)} found"

def explain(using Context) =
i"""Capture parameters and abstract captures may be constrained by a capture bound.
|Such capture bounds limit the concrete values of the capture variables and possibly
|reveal more information about the members of such captures.
|
|A lower type bound ${hl("B >: A")} expresses that the capture variable ${hl("B")}
|refers to a super capture of capture ${hl("A")}.
|
|An upper capture bound ${hl("T <: A")} declares that capture variable ${hl("T")}
|refers to a subcapture of ${hl("A")}.
|"""
}

class ClassAndCompanionNameClash(cls: Symbol, other: Symbol)(using Context)
extends NamingMsg(ClassAndCompanionNameClashID) {
def msg(using Context) =
Expand Down
Loading
Loading