Skip to content

Commit ef815fd

Browse files
authored
Adding Clause Interleaving to method definitions (#14019)
This aims to add the ability to declare functions with many clauses of type parameters, instead of at most one, and to allow those clauses to be interleaved with term clauses: ```scala def foo[A](x: A)[B](y: B) = (x, y) ``` All user-facing details can be found in the [Scala 3 new features doc](https://github.com/lampepfl/dotty/blob/2a079f92bfc05420e90987d908c41589ac94418d/docs/_docs/reference/other-new-features/generalized-method-syntax.md), and in the [SIP proposal](scala/improvement-proposals#47) The implementation details are described below in the commit messages The community's opinion of the feature can be found on [the scala contributors forum](https://contributors.scala-lang.org/t/clause-interweaving-allowing-def-f-t-x-t-u-y-u) (note however that the description there is somewhat outdated) ### Dependencies This feature has been [accepted by the SIP committee for implementation](scala/improvement-proposals#47 (comment)), it can therefore become part of the language as an experimental feature at any time. The feature will be available with `import scala.language.experimental.clauseInterleaving` ### How to make non-experimental `git revert` the commits named "Make clause interleaving experimental" and "Add import to tests" ### Future Work 1. Implement given aliases with clause interweaving: (to have types depends on terms) ```scala given myGiven[T](using x: T)[U](using y: U) = (x, y) ``` 2. Add interleaved clauses to the left-hand side of extension methods: ```scala extension (using Int)[A](using A)(a: A)[B](using B) def foo: (A, B) = ??? ``` 3. Investigate usefulness/details of clause interweaving for classes and type currying for types: ```scala class Foo[A](a: A)[B](b: B) new Foo(0)("Hello!") // type: Foo[Int][String] ? type Bar[A][B] = Map[A, B] Bar[Char] // should this mean [B] =>> Bar[Char][B] ? ``` (Started as a semester project with supervision from @smarter, now part of my missions as an intern at the scala center)
2 parents af95ceb + 65091c3 commit ef815fd

Some content is hidden

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

45 files changed

+942
-228
lines changed

Diff for: compiler/src/dotty/tools/dotc/ast/Desugar.scala

+7-5
Original file line numberDiff line numberDiff line change
@@ -915,16 +915,16 @@ object desugar {
915915
name = normalizeName(mdef, mdef.tpt).asTermName,
916916
paramss =
917917
if mdef.name.isRightAssocOperatorName then
918-
val (typaramss, paramss) = mdef.paramss.span(isTypeParamClause) // first extract type parameters
918+
val (rightTyParams, paramss) = mdef.paramss.span(isTypeParamClause) // first extract type parameters
919919

920920
paramss match
921-
case params :: paramss1 => // `params` must have a single parameter and without `given` flag
921+
case rightParam :: paramss1 => // `rightParam` must have a single parameter and without `given` flag
922922

923923
def badRightAssoc(problem: String) =
924924
report.error(em"right-associative extension method $problem", mdef.srcPos)
925925
extParamss ++ mdef.paramss
926926

927-
params match
927+
rightParam match
928928
case ValDefs(vparam :: Nil) =>
929929
if !vparam.mods.is(Given) then
930930
// we merge the extension parameters with the method parameters,
@@ -934,8 +934,10 @@ object desugar {
934934
// def %:[E](f: F)(g: G)(using H): Res = ???
935935
// will be encoded as
936936
// def %:[A](using B)[E](f: F)(c: C)(using D)(g: G)(using H): Res = ???
937-
val (leadingUsing, otherExtParamss) = extParamss.span(isUsingOrTypeParamClause)
938-
leadingUsing ::: typaramss ::: params :: otherExtParamss ::: paramss1
937+
//
938+
// If you change the names of the clauses below, also change them in right-associative-extension-methods.md
939+
val (leftTyParamsAndLeadingUsing, leftParamAndTrailingUsing) = extParamss.span(isUsingOrTypeParamClause)
940+
leftTyParamsAndLeadingUsing ::: rightTyParams ::: rightParam :: leftParamAndTrailingUsing ::: paramss1
939941
else
940942
badRightAssoc("cannot start with using clause")
941943
case _ =>

Diff for: compiler/src/dotty/tools/dotc/config/Feature.scala

+3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ object Feature:
2828
val symbolLiterals = deprecated("symbolLiterals")
2929
val fewerBraces = experimental("fewerBraces")
3030
val saferExceptions = experimental("saferExceptions")
31+
val clauseInterleaving = experimental("clauseInterleaving")
3132
val pureFunctions = experimental("pureFunctions")
3233
val captureChecking = experimental("captureChecking")
3334
val into = experimental("into")
@@ -76,6 +77,8 @@ object Feature:
7677

7778
def namedTypeArgsEnabled(using Context) = enabled(namedTypeArguments)
7879

80+
def clauseInterleavingEnabled(using Context) = enabled(clauseInterleaving)
81+
7982
def genericNumberLiteralsEnabled(using Context) = enabled(genericNumberLiterals)
8083

8184
def scala2ExperimentalMacroEnabled(using Context) = enabled(scala2macros)

Diff for: compiler/src/dotty/tools/dotc/parsing/Parsers.scala

+100-44
Original file line numberDiff line numberDiff line change
@@ -3079,6 +3079,42 @@ object Parsers {
30793079

30803080
/* -------- PARAMETERS ------------------------------------------- */
30813081

3082+
/** DefParamClauses ::= DefParamClause { DefParamClause } -- and two DefTypeParamClause cannot be adjacent
3083+
* DefParamClause ::= DefTypeParamClause
3084+
* | DefTermParamClause
3085+
* | UsingParamClause
3086+
*/
3087+
def typeOrTermParamClauses(
3088+
ownerKind: ParamOwner,
3089+
numLeadParams: Int = 0
3090+
): List[List[TypeDef] | List[ValDef]] =
3091+
3092+
def recur(firstClause: Boolean, numLeadParams: Int, prevIsTypeClause: Boolean): List[List[TypeDef] | List[ValDef]] =
3093+
newLineOptWhenFollowedBy(LPAREN)
3094+
newLineOptWhenFollowedBy(LBRACKET)
3095+
if in.token == LPAREN then
3096+
val paramsStart = in.offset
3097+
val params = termParamClause(
3098+
numLeadParams,
3099+
firstClause = firstClause)
3100+
val lastClause = params.nonEmpty && params.head.mods.flags.is(Implicit)
3101+
params :: (
3102+
if lastClause then Nil
3103+
else recur(firstClause = false, numLeadParams + params.length, prevIsTypeClause = false))
3104+
else if in.token == LBRACKET then
3105+
if prevIsTypeClause then
3106+
syntaxError(
3107+
em"Type parameter lists must be separated by a term or using parameter list",
3108+
in.offset
3109+
)
3110+
typeParamClause(ownerKind) :: recur(firstClause, numLeadParams, prevIsTypeClause = true)
3111+
else Nil
3112+
end recur
3113+
3114+
recur(firstClause = true, numLeadParams = numLeadParams, prevIsTypeClause = false)
3115+
end typeOrTermParamClauses
3116+
3117+
30823118
/** ClsTypeParamClause::= ‘[’ ClsTypeParam {‘,’ ClsTypeParam} ‘]’
30833119
* ClsTypeParam ::= {Annotation} [‘+’ | ‘-’]
30843120
* id [HkTypeParamClause] TypeParamBounds
@@ -3132,34 +3168,39 @@ object Parsers {
31323168

31333169
/** ContextTypes ::= FunArgType {‘,’ FunArgType}
31343170
*/
3135-
def contextTypes(ofClass: Boolean, nparams: Int, impliedMods: Modifiers): List[ValDef] =
3171+
def contextTypes(ofClass: Boolean, numLeadParams: Int, impliedMods: Modifiers): List[ValDef] =
31363172
val tps = commaSeparated(funArgType)
3137-
var counter = nparams
3173+
var counter = numLeadParams
31383174
def nextIdx = { counter += 1; counter }
31393175
val paramFlags = if ofClass then LocalParamAccessor else Param
31403176
tps.map(makeSyntheticParameter(nextIdx, _, paramFlags | Synthetic | impliedMods.flags))
31413177

3142-
/** ClsParamClause ::= ‘(’ [‘erased’] ClsParams ‘)’ | UsingClsParamClause
3143-
* UsingClsParamClause::= ‘(’ ‘using’ [‘erased’] (ClsParams | ContextTypes) ‘)’
3178+
/** ClsTermParamClause ::= ‘(’ [‘erased’] ClsParams ‘)’ | UsingClsTermParamClause
3179+
* UsingClsTermParamClause::= ‘(’ ‘using’ [‘erased’] (ClsParams | ContextTypes) ‘)’
31443180
* ClsParams ::= ClsParam {‘,’ ClsParam}
31453181
* ClsParam ::= {Annotation}
3182+
*
3183+
* TypelessClause ::= DefTermParamClause
3184+
* | UsingParamClause
31463185
*
3147-
* DefParamClause ::= ‘(’ [‘erased’] DefParams ‘)’ | UsingParamClause
3148-
* UsingParamClause ::= ‘(’ ‘using’ [‘erased’] (DefParams | ContextTypes) ‘)’
3149-
* DefParams ::= DefParam {‘,’ DefParam}
3150-
* DefParam ::= {Annotation} [‘inline’] Param
3186+
* DefTermParamClause::= [nl] ‘(’ [DefTermParams] ‘)’
3187+
* UsingParamClause ::= ‘(’ ‘using’ [‘erased’] (DefTermParams | ContextTypes) ‘)’
3188+
* DefImplicitClause ::= [nl] ‘(’ ‘implicit’ DefTermParams ‘)’
3189+
* DefTermParams ::= DefTermParam {‘,’ DefTermParam}
3190+
* DefTermParam ::= {Annotation} [‘inline’] Param
31513191
*
31523192
* Param ::= id `:' ParamType [`=' Expr]
31533193
*
31543194
* @return the list of parameter definitions
31553195
*/
3156-
def paramClause(nparams: Int, // number of parameters preceding this clause
3157-
ofClass: Boolean = false, // owner is a class
3158-
ofCaseClass: Boolean = false, // owner is a case class
3159-
prefix: Boolean = false, // clause precedes name of an extension method
3160-
givenOnly: Boolean = false, // only given parameters allowed
3161-
firstClause: Boolean = false // clause is the first in regular list of clauses
3162-
): List[ValDef] = {
3196+
def termParamClause(
3197+
numLeadParams: Int, // number of parameters preceding this clause
3198+
ofClass: Boolean = false, // owner is a class
3199+
ofCaseClass: Boolean = false, // owner is a case class
3200+
prefix: Boolean = false, // clause precedes name of an extension method
3201+
givenOnly: Boolean = false, // only given parameters allowed
3202+
firstClause: Boolean = false // clause is the first in regular list of clauses
3203+
): List[ValDef] = {
31633204
var impliedMods: Modifiers = EmptyModifiers
31643205

31653206
def addParamMod(mod: () => Mod) = impliedMods = addMod(impliedMods, atSpan(in.skipToken()) { mod() })
@@ -3224,7 +3265,7 @@ object Parsers {
32243265
checkVarArgsRules(rest)
32253266
}
32263267

3227-
// begin paramClause
3268+
// begin termParamClause
32283269
inParens {
32293270
if in.token == RPAREN && !prefix && !impliedMods.is(Given) then Nil
32303271
else
@@ -3239,41 +3280,43 @@ object Parsers {
32393280
|| startParamTokens.contains(in.token)
32403281
|| isIdent && (in.name == nme.inline || in.lookahead.isColon)
32413282
if isParams then commaSeparated(() => param())
3242-
else contextTypes(ofClass, nparams, impliedMods)
3283+
else contextTypes(ofClass, numLeadParams, impliedMods)
32433284
checkVarArgsRules(clause)
32443285
clause
32453286
}
32463287
}
32473288

3248-
/** ClsParamClauses ::= {ClsParamClause} [[nl] ‘(’ [‘implicit’] ClsParams ‘)’]
3249-
* DefParamClauses ::= {DefParamClause} [[nl] ‘(’ [‘implicit’] DefParams ‘)’]
3289+
/** ClsTermParamClauses ::= {ClsTermParamClause} [[nl] ‘(’ [‘implicit’] ClsParams ‘)’]
3290+
* TypelessClauses ::= TypelessClause {TypelessClause}
32503291
*
32513292
* @return The parameter definitions
32523293
*/
3253-
def paramClauses(ofClass: Boolean = false,
3254-
ofCaseClass: Boolean = false,
3255-
givenOnly: Boolean = false,
3256-
numLeadParams: Int = 0): List[List[ValDef]] =
3294+
def termParamClauses(
3295+
ofClass: Boolean = false,
3296+
ofCaseClass: Boolean = false,
3297+
givenOnly: Boolean = false,
3298+
numLeadParams: Int = 0
3299+
): List[List[ValDef]] =
32573300

3258-
def recur(firstClause: Boolean, nparams: Int): List[List[ValDef]] =
3301+
def recur(firstClause: Boolean, numLeadParams: Int): List[List[ValDef]] =
32593302
newLineOptWhenFollowedBy(LPAREN)
32603303
if in.token == LPAREN then
32613304
val paramsStart = in.offset
3262-
val params = paramClause(
3263-
nparams,
3305+
val params = termParamClause(
3306+
numLeadParams,
32643307
ofClass = ofClass,
32653308
ofCaseClass = ofCaseClass,
32663309
givenOnly = givenOnly,
32673310
firstClause = firstClause)
32683311
val lastClause = params.nonEmpty && params.head.mods.flags.is(Implicit)
32693312
params :: (
32703313
if lastClause then Nil
3271-
else recur(firstClause = false, nparams + params.length))
3314+
else recur(firstClause = false, numLeadParams + params.length))
32723315
else Nil
32733316
end recur
32743317

32753318
recur(firstClause = true, numLeadParams)
3276-
end paramClauses
3319+
end termParamClauses
32773320

32783321
/* -------- DEFS ------------------------------------------- */
32793322

@@ -3514,11 +3557,15 @@ object Parsers {
35143557
}
35153558
}
35163559

3560+
3561+
35173562
/** DefDef ::= DefSig [‘:’ Type] ‘=’ Expr
3518-
* | this ParamClause ParamClauses `=' ConstrExpr
3563+
* | this TypelessClauses [DefImplicitClause] `=' ConstrExpr
35193564
* DefDcl ::= DefSig `:' Type
3520-
* DefSig ::= id [DefTypeParamClause] DefParamClauses
3521-
* | ExtParamClause [nl] [‘.’] id DefParamClauses
3565+
* DefSig ::= id [DefTypeParamClause] DefTermParamClauses
3566+
*
3567+
* if clauseInterleaving is enabled:
3568+
* DefSig ::= id [DefParamClauses] [DefImplicitClause]
35223569
*/
35233570
def defDefOrDcl(start: Offset, mods: Modifiers, numLeadParams: Int = 0): DefDef = atSpan(start, nameStart) {
35243571

@@ -3537,7 +3584,7 @@ object Parsers {
35373584

35383585
if (in.token == THIS) {
35393586
in.nextToken()
3540-
val vparamss = paramClauses(numLeadParams = numLeadParams)
3587+
val vparamss = termParamClauses(numLeadParams = numLeadParams)
35413588
if (vparamss.isEmpty || vparamss.head.take(1).exists(_.mods.isOneOf(GivenOrImplicit)))
35423589
in.token match {
35433590
case LBRACKET => syntaxError(em"no type parameters allowed here")
@@ -3555,9 +3602,18 @@ object Parsers {
35553602
val mods1 = addFlag(mods, Method)
35563603
val ident = termIdent()
35573604
var name = ident.name.asTermName
3558-
val tparams = typeParamClauseOpt(ParamOwner.Def)
3559-
val vparamss = paramClauses(numLeadParams = numLeadParams)
3605+
val paramss =
3606+
if in.featureEnabled(Feature.clauseInterleaving) then
3607+
// If you are making interleaving stable manually, please refer to the PR introducing it instead, section "How to make non-experimental"
3608+
typeOrTermParamClauses(ParamOwner.Def, numLeadParams = numLeadParams)
3609+
else
3610+
val tparams = typeParamClauseOpt(ParamOwner.Def)
3611+
val vparamss = termParamClauses(numLeadParams = numLeadParams)
3612+
3613+
joinParams(tparams, vparamss)
3614+
35603615
var tpt = fromWithinReturnType { typedOpt() }
3616+
35613617
if (migrateTo3) newLineOptWhenFollowedBy(LBRACE)
35623618
val rhs =
35633619
if in.token == EQUALS then
@@ -3574,7 +3630,7 @@ object Parsers {
35743630
accept(EQUALS)
35753631
expr()
35763632

3577-
val ddef = DefDef(name, joinParams(tparams, vparamss), tpt, rhs)
3633+
val ddef = DefDef(name, paramss, tpt, rhs)
35783634
if (isBackquoted(ident)) ddef.pushAttachment(Backquoted, ())
35793635
finalizeDef(ddef, mods1, start)
35803636
}
@@ -3695,12 +3751,12 @@ object Parsers {
36953751
val templ = templateOpt(constr)
36963752
finalizeDef(TypeDef(name, templ), mods, start)
36973753

3698-
/** ClassConstr ::= [ClsTypeParamClause] [ConstrMods] ClsParamClauses
3754+
/** ClassConstr ::= [ClsTypeParamClause] [ConstrMods] ClsTermParamClauses
36993755
*/
37003756
def classConstr(isCaseClass: Boolean = false): DefDef = atSpan(in.lastOffset) {
37013757
val tparams = typeParamClauseOpt(ParamOwner.Class)
37023758
val cmods = fromWithinClassConstr(constrModsOpt())
3703-
val vparamss = paramClauses(ofClass = true, ofCaseClass = isCaseClass)
3759+
val vparamss = termParamClauses(ofClass = true, ofCaseClass = isCaseClass)
37043760
makeConstructor(tparams, vparamss).withMods(cmods)
37053761
}
37063762

@@ -3802,7 +3858,7 @@ object Parsers {
38023858
newLineOpt()
38033859
val vparamss =
38043860
if in.token == LPAREN && in.lookahead.isIdent(nme.using)
3805-
then paramClauses(givenOnly = true)
3861+
then termParamClauses(givenOnly = true)
38063862
else Nil
38073863
newLinesOpt()
38083864
val noParams = tparams.isEmpty && vparamss.isEmpty
@@ -3837,32 +3893,32 @@ object Parsers {
38373893
finalizeDef(gdef, mods1, start)
38383894
}
38393895

3840-
/** Extension ::= ‘extension’ [DefTypeParamClause] {UsingParamClause} ‘(’ DefParam ‘)’
3896+
/** Extension ::= ‘extension’ [DefTypeParamClause] {UsingParamClause} ‘(’ DefTermParam ‘)’
38413897
* {UsingParamClause} ExtMethods
38423898
*/
38433899
def extension(): ExtMethods =
38443900
val start = in.skipToken()
38453901
val tparams = typeParamClauseOpt(ParamOwner.Def)
38463902
val leadParamss = ListBuffer[List[ValDef]]()
3847-
def nparams = leadParamss.map(_.length).sum
3903+
def numLeadParams = leadParamss.map(_.length).sum
38483904
while
3849-
val extParams = paramClause(nparams, prefix = true)
3905+
val extParams = termParamClause(numLeadParams, prefix = true)
38503906
leadParamss += extParams
38513907
isUsingClause(extParams)
38523908
do ()
3853-
leadParamss ++= paramClauses(givenOnly = true, numLeadParams = nparams)
3909+
leadParamss ++= termParamClauses(givenOnly = true, numLeadParams = numLeadParams)
38543910
if in.isColon then
38553911
syntaxError(em"no `:` expected here")
38563912
in.nextToken()
38573913
val methods: List[Tree] =
38583914
if in.token == EXPORT then
38593915
exportClause()
38603916
else if isDefIntro(modifierTokens) then
3861-
extMethod(nparams) :: Nil
3917+
extMethod(numLeadParams) :: Nil
38623918
else
38633919
in.observeIndented()
38643920
newLineOptWhenFollowedBy(LBRACE)
3865-
if in.isNestedStart then inDefScopeBraces(extMethods(nparams))
3921+
if in.isNestedStart then inDefScopeBraces(extMethods(numLeadParams))
38663922
else { syntaxErrorOrIncomplete(em"Extension without extension methods") ; Nil }
38673923
val result = atSpan(start)(ExtMethods(joinParams(tparams, leadParamss.toList), methods))
38683924
val comment = in.getDocComment(start)

Diff for: compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala

+15-14
Original file line numberDiff line numberDiff line change
@@ -896,30 +896,31 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
896896
if isExtension then
897897
val paramss =
898898
if tree.name.isRightAssocOperatorName then
899+
// If you change the names of the clauses below, also change them in right-associative-extension-methods.md
899900
// we have the following encoding of tree.paramss:
900-
// (leadingTyParamss ++ leadingUsing
901-
// ++ rightTyParamss ++ rightParamss
902-
// ++ leftParamss ++ trailingUsing ++ rest)
901+
// (leftTyParams ++ leadingUsing
902+
// ++ rightTyParams ++ rightParam
903+
// ++ leftParam ++ trailingUsing ++ rest)
903904
// e.g.
904905
// extension [A](using B)(c: C)(using D)
905906
// def %:[E](f: F)(g: G)(using H): Res = ???
906907
// will have the following values:
907-
// - leadingTyParamss = List(`[A]`)
908+
// - leftTyParams = List(`[A]`)
908909
// - leadingUsing = List(`(using B)`)
909-
// - rightTyParamss = List(`[E]`)
910-
// - rightParamss = List(`(f: F)`)
911-
// - leftParamss = List(`(c: C)`)
910+
// - rightTyParams = List(`[E]`)
911+
// - rightParam = List(`(f: F)`)
912+
// - leftParam = List(`(c: C)`)
912913
// - trailingUsing = List(`(using D)`)
913914
// - rest = List(`(g: G)`, `(using H)`)
914-
// we need to swap (rightTyParams ++ rightParamss) with (leftParamss ++ trailingUsing)
915-
val (leadingTyParamss, rest1) = tree.paramss.span(isTypeParamClause)
915+
// we need to swap (rightTyParams ++ rightParam) with (leftParam ++ trailingUsing)
916+
val (leftTyParams, rest1) = tree.paramss.span(isTypeParamClause)
916917
val (leadingUsing, rest2) = rest1.span(isUsingClause)
917-
val (rightTyParamss, rest3) = rest2.span(isTypeParamClause)
918-
val (rightParamss, rest4) = rest3.splitAt(1)
919-
val (leftParamss, rest5) = rest4.splitAt(1)
918+
val (rightTyParams, rest3) = rest2.span(isTypeParamClause)
919+
val (rightParam, rest4) = rest3.splitAt(1)
920+
val (leftParam, rest5) = rest4.splitAt(1)
920921
val (trailingUsing, rest6) = rest5.span(isUsingClause)
921-
if leftParamss.nonEmpty then
922-
leadingTyParamss ::: leadingUsing ::: leftParamss ::: trailingUsing ::: rightTyParamss ::: rightParamss ::: rest6
922+
if leftParam.nonEmpty then
923+
leftTyParams ::: leadingUsing ::: leftParam ::: trailingUsing ::: rightTyParams ::: rightParam ::: rest6
923924
else
924925
tree.paramss // it wasn't a binary operator, after all.
925926
else

0 commit comments

Comments
 (0)