From 707f0c94857c4c96961d42fe1cd40b504be5aa2a Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Mon, 15 Dec 2025 09:28:15 -0800 Subject: [PATCH 1/4] Status quo extension parameters --- tests/neg/i24745.scala | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/neg/i24745.scala diff --git a/tests/neg/i24745.scala b/tests/neg/i24745.scala new file mode 100644 index 000000000000..fa264a139248 --- /dev/null +++ b/tests/neg/i24745.scala @@ -0,0 +1,17 @@ +extension (s: String) + def f_::(using DummyImplicit): String = s.reverse // error + def g_::(x: (suffix: String, n: Int)): String = s"$s${x.suffix * x.n}" + def ok_::(using suffix: String, n: Int): String = s"$s${suffix * n}" // error + def no_::(suffix: String, n: Int): String = s"$s${suffix * n}" // error + +@main def Test = + println: + "hello, world".f_:: + println: + (suffix = "s", n = 3).g_::("hello, world") + println: + "hello, world" g_:: ("s", 3) + println: + given String = "s" + given Int = 3 + "hello, world".ok_:: From 013314172a317f29e9bc983ab55da88c8692985d Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Tue, 16 Dec 2025 07:55:54 -0800 Subject: [PATCH 2/4] Refactor extMethod --- .../src/dotty/tools/dotc/ast/Desugar.scala | 92 ++++++++++--------- 1 file changed, 48 insertions(+), 44 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index ab77350be26c..8a55008496ad 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -9,7 +9,7 @@ import Decorators.* import Annotations.Annotation import NameKinds.{UniqueName, ContextBoundParamName, ContextFunctionParamName, DefaultGetterName, WildcardParamName} import typer.{Namer, Checking} -import util.{Property, SourceFile, SourcePosition, SrcPos, Chars} +import util.{Chars, NoSourcePosition, Property, SourceFile, SourcePosition, SrcPos} import config.{Feature, Config} import config.Feature.{sourceVersion, migrateTo3, enabled} import config.SourceVersion.* @@ -1185,55 +1185,59 @@ object desugar { } def extMethod(mdef: DefDef, extParamss: List[ParamClause])(using Context): DefDef = + def finish(problem: String = "", pos: SrcPos = NoSourcePosition) = + if !problem.isEmpty then + report.error(em"right-associative extension method $problem", pos) + extParamss ++ mdef.paramss + def rightAssocParams = + val (rightTyParams, paramss) = mdef.paramss.span(isTypeParamClause) // first extract type parameters + + paramss match + case rightParam :: paramss1 => // `rightParam` must have a single parameter and without `given` flag + rightParam match + case ValDefs(vparam :: Nil) => + if !vparam.mods.is(Given) then + // we merge the extension parameters with the method parameters, + // swapping the operator arguments: + // e.g. + // extension [A](using B)(c: C)(using D) + // def %:[E](f: F)(g: G)(using H): Res = ??? + // will be encoded as + // def %:[A](using B)[E](f: F)(c: C)(using D)(g: G)(using H): Res = ??? + // + // If you change the names of the clauses below, also change them in right-associative-extension-methods.md + val (leftTyParamsAndLeadingUsing, leftParamAndTrailingUsing) = extParamss.span(isUsingOrTypeParamClause) + + val names = (for ps <- mdef.paramss; p <- ps yield p.name).toSet[Name] + + val tt = new untpd.UntypedTreeTraverser: + def traverse(tree: Tree)(using Context): Unit = tree match + case tree: Ident if names.contains(tree.name) => + finish(s"cannot have a forward reference to ${tree.name}", tree.srcPos) + case _ => traverseChildren(tree) + + for ts <- leftParamAndTrailingUsing; t <- ts do + tt.traverse(t) + + leftTyParamsAndLeadingUsing ::: rightTyParams ::: rightParam :: leftParamAndTrailingUsing ::: paramss1 + else + finish("cannot start with using clause", mdef.srcPos) + case _ => + finish("must start with a single parameter", mdef.srcPos) + case _ => + // no value parameters, so not an infix operator. + finish() + end rightAssocParams + cpy.DefDef(mdef)( name = normalizeName(mdef, mdef.tpt).asTermName, paramss = if mdef.name.isRightAssocOperatorName then - val (rightTyParams, paramss) = mdef.paramss.span(isTypeParamClause) // first extract type parameters - - paramss match - case rightParam :: paramss1 => // `rightParam` must have a single parameter and without `given` flag - - def badRightAssoc(problem: String, pos: SrcPos) = - report.error(em"right-associative extension method $problem", pos) - extParamss ++ mdef.paramss - - rightParam match - case ValDefs(vparam :: Nil) => - if !vparam.mods.is(Given) then - // we merge the extension parameters with the method parameters, - // swapping the operator arguments: - // e.g. - // extension [A](using B)(c: C)(using D) - // def %:[E](f: F)(g: G)(using H): Res = ??? - // will be encoded as - // def %:[A](using B)[E](f: F)(c: C)(using D)(g: G)(using H): Res = ??? - // - // If you change the names of the clauses below, also change them in right-associative-extension-methods.md - val (leftTyParamsAndLeadingUsing, leftParamAndTrailingUsing) = extParamss.span(isUsingOrTypeParamClause) - - val names = (for ps <- mdef.paramss; p <- ps yield p.name).toSet[Name] - - val tt = new untpd.UntypedTreeTraverser: - def traverse(tree: Tree)(using Context): Unit = tree match - case tree: Ident if names.contains(tree.name) => - badRightAssoc(s"cannot have a forward reference to ${tree.name}", tree.srcPos) - case _ => traverseChildren(tree) - - for ts <- leftParamAndTrailingUsing; t <- ts do - tt.traverse(t) - - leftTyParamsAndLeadingUsing ::: rightTyParams ::: rightParam :: leftParamAndTrailingUsing ::: paramss1 - else - badRightAssoc("cannot start with using clause", mdef.srcPos) - case _ => - badRightAssoc("must start with a single parameter", mdef.srcPos) - case _ => - // no value parameters, so not an infix operator. - extParamss ++ mdef.paramss + rightAssocParams else - extParamss ++ mdef.paramss + finish() ).withMods(mdef.mods | ExtensionMethod) + end extMethod /** Transform extension construct to list of extension methods */ def extMethods(ext: ExtMethods)(using Context): Tree = flatTree { From 7eed54408602ea65a00791fb4040c073dbd0e929 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Tue, 16 Dec 2025 08:01:46 -0800 Subject: [PATCH 3/4] Accept using in rightAssocParams --- .../src/dotty/tools/dotc/ast/Desugar.scala | 58 +++++++++---------- tests/neg/i24745.scala | 4 +- tests/neg/rightassoc-extmethod.check | 6 +- tests/neg/rightassoc-extmethod.scala | 3 +- 4 files changed, 33 insertions(+), 38 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 8a55008496ad..0979e09d7e17 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1193,37 +1193,37 @@ object desugar { val (rightTyParams, paramss) = mdef.paramss.span(isTypeParamClause) // first extract type parameters paramss match - case rightParam :: paramss1 => // `rightParam` must have a single parameter and without `given` flag - rightParam match - case ValDefs(vparam :: Nil) => - if !vparam.mods.is(Given) then - // we merge the extension parameters with the method parameters, - // swapping the operator arguments: - // e.g. - // extension [A](using B)(c: C)(using D) - // def %:[E](f: F)(g: G)(using H): Res = ??? - // will be encoded as - // def %:[A](using B)[E](f: F)(c: C)(using D)(g: G)(using H): Res = ??? - // - // If you change the names of the clauses below, also change them in right-associative-extension-methods.md - val (leftTyParamsAndLeadingUsing, leftParamAndTrailingUsing) = extParamss.span(isUsingOrTypeParamClause) - - val names = (for ps <- mdef.paramss; p <- ps yield p.name).toSet[Name] - - val tt = new untpd.UntypedTreeTraverser: - def traverse(tree: Tree)(using Context): Unit = tree match - case tree: Ident if names.contains(tree.name) => - finish(s"cannot have a forward reference to ${tree.name}", tree.srcPos) - case _ => traverseChildren(tree) + case (rightParam @ ValDefs(vparam :: Nil)) :: paramss if !vparam.mods.is(Given) => + // must be a single parameter without `given` flag for rassoc rewrite + // we merge the extension parameters with the method parameters, + // swapping the operator arguments: + // e.g. + // extension [A](using B)(c: C)(using D) + // def %:[E](f: F)(g: G)(using H): Res = ??? + // will be encoded as + // def %:[A](using B)[E](f: F)(c: C)(using D)(g: G)(using H): Res = ??? + // + // If you change the names in the clauses below, also change them in right-associative-extension-methods.md + val (leftTyParamsAndLeadingUsing, leftParamAndTrailingUsing) = extParamss.span(isUsingOrTypeParamClause) + + val names = (for ps <- mdef.paramss; p <- ps yield p.name).toSet[Name] + + val tt = new untpd.UntypedTreeTraverser: + def traverse(tree: Tree)(using Context): Unit = tree match + case tree: Ident if names.contains(tree.name) => + finish(s"cannot have a forward reference to ${tree.name}", tree.srcPos) + case _ => traverseChildren(tree) - for ts <- leftParamAndTrailingUsing; t <- ts do - tt.traverse(t) + for ts <- leftParamAndTrailingUsing; t <- ts do + tt.traverse(t) - leftTyParamsAndLeadingUsing ::: rightTyParams ::: rightParam :: leftParamAndTrailingUsing ::: paramss1 - else - finish("cannot start with using clause", mdef.srcPos) - case _ => - finish("must start with a single parameter", mdef.srcPos) + leftTyParamsAndLeadingUsing ::: rightTyParams ::: rightParam :: leftParamAndTrailingUsing ::: paramss + case ValDefs(vparam :: _) :: _ => + if vparam.mods.is(Given) then + // no explicit value parameters, so not an infix operator. + finish() + else + finish("must start with a single parameter, consider a tupled parameter instead", mdef.srcPos) case _ => // no value parameters, so not an infix operator. finish() diff --git a/tests/neg/i24745.scala b/tests/neg/i24745.scala index fa264a139248..6d3ccb6d5b86 100644 --- a/tests/neg/i24745.scala +++ b/tests/neg/i24745.scala @@ -1,7 +1,7 @@ extension (s: String) - def f_::(using DummyImplicit): String = s.reverse // error + def f_::(using DummyImplicit): String = s.reverse def g_::(x: (suffix: String, n: Int)): String = s"$s${x.suffix * x.n}" - def ok_::(using suffix: String, n: Int): String = s"$s${suffix * n}" // error + def ok_::(using suffix: String, n: Int): String = s"$s${suffix * n}" def no_::(suffix: String, n: Int): String = s"$s${suffix * n}" // error @main def Test = diff --git a/tests/neg/rightassoc-extmethod.check b/tests/neg/rightassoc-extmethod.check index a1d2328ed2ff..80f02851474c 100644 --- a/tests/neg/rightassoc-extmethod.check +++ b/tests/neg/rightassoc-extmethod.check @@ -1,8 +1,4 @@ --- Error: tests/neg/rightassoc-extmethod.scala:1:23 -------------------------------------------------------------------- -1 |extension (x: Int) def +: (using String): Int = x // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | right-associative extension method cannot start with using clause -- Error: tests/neg/rightassoc-extmethod.scala:2:23 -------------------------------------------------------------------- 2 |extension (x: Int) def *: (y: Int, z: Int) = x // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | right-associative extension method must start with a single parameter + | right-associative extension method must start with a single parameter, consider a tupled parameter instead diff --git a/tests/neg/rightassoc-extmethod.scala b/tests/neg/rightassoc-extmethod.scala index 4a136ca6eac3..9fbea9d4a04a 100644 --- a/tests/neg/rightassoc-extmethod.scala +++ b/tests/neg/rightassoc-extmethod.scala @@ -1,3 +1,2 @@ -extension (x: Int) def +: (using String): Int = x // error +extension (x: Int) def +: (using String): Int = x extension (x: Int) def *: (y: Int, z: Int) = x // error - From a37f089cb34e4b19255e6bc6a786e47cd71deca8 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Tue, 16 Dec 2025 10:45:22 -0800 Subject: [PATCH 4/4] Test arg order --- tests/neg/i24745.scala | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/neg/i24745.scala b/tests/neg/i24745.scala index 6d3ccb6d5b86..855bad5e7b0f 100644 --- a/tests/neg/i24745.scala +++ b/tests/neg/i24745.scala @@ -3,10 +3,19 @@ extension (s: String) def g_::(x: (suffix: String, n: Int)): String = s"$s${x.suffix * x.n}" def ok_::(using suffix: String, n: Int): String = s"$s${suffix * n}" def no_::(suffix: String, n: Int): String = s"$s${suffix * n}" // error + def huh_::(using DummyImplicit)(suffix: String, n: Int): String = s"$s${suffix * n}" + +def local = + extension (s: String) def f_::(using DummyImplicit): String = s.reverse + "hello, world".f_:: @main def Test = println: "hello, world".f_:: + println: + f_:: + ("hello, world") + (using DummyImplicit.dummyImplicit) println: (suffix = "s", n = 3).g_::("hello, world") println: @@ -15,3 +24,5 @@ extension (s: String) given String = "s" given Int = 3 "hello, world".ok_:: + println: + "hello, world".huh_::("s", 3)