From d764b20d7361dbd6144743a69e1db0111c1e74d5 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Jan 2024 13:22:06 +0100 Subject: [PATCH 01/63] New modularity language import --- .../src/scala/runtime/stdLibPatches/language.scala | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index 70d5f2d41907..49baa9bc2df6 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -91,6 +91,17 @@ object language: @compileTimeOnly("`into` can only be used at compile time in import statements") object into + /** Experimental support for new features for better modularity, including + * - better tracking of dependencies through classes + * - better usability of context bounds + * - better syntax and conventions for type classes + * - ability to merge exported types in intersections + * + * @see [[https://dotty.epfl.ch/docs/reference/experimental/modularity]] + */ + @compileTimeOnly("`modularity` can only be used at compile time in import statements") + object modularity + /** Was needed to add support for relaxed imports of extension methods. * The language import is no longer needed as this is now a standard feature since SIP was accepted. * @see [[http://dotty.epfl.ch/docs/reference/contextual/extension-methods]] From 34a49ca939595ad3c3b25285b47f3809c44864c0 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 15 Dec 2023 16:16:58 +0100 Subject: [PATCH 02/63] A relaxation concerning exported type aliases The rules for export forwarders are changed as follows. Previously, all export forwarders were declared `final`. Now, only term members are declared `final`. Type aliases left aside. This makes it possible to export the same type member into several traits and then mix these traits in the same class. `typeclass-aggregates.scala` shows why this is essential to be able to combine multiple givens with type members. The change does not lose safety since different type aliases would in any case lead to uninstantiatable classes. --- .../src/dotty/tools/dotc/config/Feature.scala | 1 + .../src/dotty/tools/dotc/core/Flags.scala | 2 - .../src/dotty/tools/dotc/typer/Namer.scala | 6 ++- .../reference/other-new-features/export.md | 16 +++++-- tests/neg/i0248-inherit-refined.check | 12 +++++ tests/pos/typeclass-aggregates.scala | 47 +++++++++++++++++++ 6 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 tests/neg/i0248-inherit-refined.check create mode 100644 tests/pos/typeclass-aggregates.scala diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 7eb95badd4d0..5efd0f51cb1c 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -32,6 +32,7 @@ object Feature: val pureFunctions = experimental("pureFunctions") val captureChecking = experimental("captureChecking") val into = experimental("into") + val modularity = experimental("modularity") /** Is `feature` enabled by by a command-line setting? The enabling setting is * diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index 249940d8ff99..6bbdedcb006b 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -540,8 +540,6 @@ object Flags { /** Flags retained in type export forwarders */ val RetainedExportTypeFlags = Infix - val MandatoryExportTypeFlags = Exported | Final - /** Flags that apply only to classes */ val ClassOnlyFlags = Sealed | Open | Abstract.toTypeFlags diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index c1bde8d4fd3c..4dffee08bcaa 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -26,7 +26,7 @@ import Nullables.* import transform.ValueClasses.* import TypeErasure.erasure import reporting.* -import config.Feature.sourceVersion +import config.Feature.{sourceVersion, modularity} import config.SourceVersion.* import scala.compiletime.uninitialized @@ -1203,7 +1203,9 @@ class Namer { typer: Typer => target = target.etaExpand newSymbol( cls, forwarderName, - MandatoryExportTypeFlags | (sym.flags & RetainedExportTypeFlags), + Exported + | (sym.flags & RetainedExportTypeFlags) + | (if Feature.enabled(modularity) then EmptyFlags else Final), TypeAlias(target), coord = span) // Note: This will always create unparameterzied aliases. So even if the original type is diff --git a/docs/_docs/reference/other-new-features/export.md b/docs/_docs/reference/other-new-features/export.md index 98e9a7d3d711..e21d369b6b5e 100644 --- a/docs/_docs/reference/other-new-features/export.md +++ b/docs/_docs/reference/other-new-features/export.md @@ -37,7 +37,12 @@ final def print(bits: BitMap): Unit = printUnit.print(bits) final type PrinterType = printUnit.PrinterType ``` -They can be accessed inside `Copier` as well as from outside: +With the experimental `modularity` language import, only exported methods and values are final, whereas the generated `PrinterType` would be a simple type alias +```scala + type PrinterType = printUnit.PrinterType +``` + +These aliases can be accessed inside `Copier` as well as from outside: ```scala val copier = new Copier @@ -90,12 +95,17 @@ export O.* ``` Export aliases copy the type and value parameters of the members they refer to. -Export aliases are always `final`. Aliases of given instances are again defined as givens (and aliases of old-style implicits are `implicit`). Aliases of extensions are again defined as extensions. Aliases of inline methods or values are again defined `inline`. There are no other modifiers that can be given to an alias. This has the following consequences for overriding: +Export aliases of term members are always `final`. Aliases of given instances are again defined as givens (and aliases of old-style implicits are `implicit`). Aliases of extensions are again defined as extensions. Aliases of inline methods or values are again defined `inline`. There are no other modifiers that can be given to an alias. This has the following consequences for overriding: - - Export aliases cannot be overridden, since they are final. + - Export aliases of methods or fields cannot be overridden, since they are final. - Export aliases cannot override concrete members in base classes, since they are not marked `override`. - However, export aliases can implement deferred members of base classes. + - Export type aliases are normally also final, except when the experimental + language import `modularity` is present. The general + rules for type aliases ensure in any case that if there are several type aliases in a class, + they must agree on their right hand sides, or the class could not be instantiated. + So dropping the `final` for export type aliases is safe. Export aliases for public value definitions that are accessed without referring to private values in the qualifier path diff --git a/tests/neg/i0248-inherit-refined.check b/tests/neg/i0248-inherit-refined.check new file mode 100644 index 000000000000..4e14c3c6f14b --- /dev/null +++ b/tests/neg/i0248-inherit-refined.check @@ -0,0 +1,12 @@ +-- [E170] Type Error: tests/neg/i0248-inherit-refined.scala:8:18 ------------------------------------------------------- +8 | class C extends Y // error + | ^ + | test.A & test.B is not a class type + | + | longer explanation available when compiling with `-explain` +-- [E170] Type Error: tests/neg/i0248-inherit-refined.scala:10:18 ------------------------------------------------------ +10 | class D extends Z // error + | ^ + | test.A | test.B is not a class type + | + | longer explanation available when compiling with `-explain` diff --git a/tests/pos/typeclass-aggregates.scala b/tests/pos/typeclass-aggregates.scala new file mode 100644 index 000000000000..77b0f1a9f04a --- /dev/null +++ b/tests/pos/typeclass-aggregates.scala @@ -0,0 +1,47 @@ +//> using options -source future -language:experimental.modularity +trait Ord: + type This + extension (x: This) + def compareTo(y: This): Int + def < (y: This): Boolean = compareTo(y) < 0 + def > (y: This): Boolean = compareTo(y) > 0 + + trait OrdProxy extends Ord: + export Ord.this.* + +trait SemiGroup: + type This + extension (x: This) def combine(y: This): This + + trait SemiGroupProxy extends SemiGroup: + export SemiGroup.this.* + +trait Monoid extends SemiGroup: + def unit: This + + trait MonoidProxy extends Monoid: + export Monoid.this.* + +def ordWithMonoid(ord: Ord, monoid: Monoid{ type This = ord.This }): Ord & Monoid = + new ord.OrdProxy with monoid.MonoidProxy {} + +trait OrdWithMonoid extends Ord, Monoid + +def ordWithMonoid2(ord: Ord, monoid: Monoid{ type This = ord.This }) = //: OrdWithMonoid { type This = ord.This} = + new OrdWithMonoid with ord.OrdProxy with monoid.MonoidProxy {} + +given intOrd: Ord { type This = Int } = ??? +given intMonoid: Monoid { type This = Int } = ??? + +//given (using ord: Ord, monoid: Monoid{ type This = ord.This }): (Ord & Monoid { type This = ord.This}) = +// ordWithMonoid2(ord, monoid) + +val x = summon[Ord & Monoid { type This = Int}] +val y: Int = ??? : x.This + +// given [A, B](using ord: A is Ord, monoid: A is Monoid) => A is Ord & Monoid = +// new ord.OrdProxy with monoid.MonoidProxy {} + +given [A](using ord: Ord { type This = A }, monoid: Monoid { type This = A}): (Ord & Monoid) { type This = A} = + new ord.OrdProxy with monoid.MonoidProxy {} + From 8fe26f7c15167b8350ddd7ae580ede391f1055a3 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 13 Dec 2023 10:54:15 +0100 Subject: [PATCH 03/63] Allow class parents to be refined types. Refinements of a class parent are added as synthetic members to the inheriting class. --- .../src/dotty/tools/dotc/core/NamerOps.scala | 21 +++++ .../tools/dotc/core/tasty/TreeUnpickler.scala | 2 +- .../tools/dotc/transform/init/Util.scala | 1 + .../src/dotty/tools/dotc/typer/Namer.scala | 37 +++++++-- .../src/dotty/tools/dotc/typer/Typer.scala | 30 +++++-- tests/neg/i0248-inherit-refined.scala | 6 +- tests/neg/parent-refinement-access.check | 7 ++ tests/neg/parent-refinement-access.scala | 6 ++ tests/neg/parent-refinement.check | 29 ++++++- tests/neg/parent-refinement.scala | 20 ++++- tests/pos/parent-refinement.scala | 48 +++++++++++ tests/pos/typeclasses.scala | 79 ++++--------------- 12 files changed, 200 insertions(+), 86 deletions(-) create mode 100644 tests/neg/parent-refinement-access.check create mode 100644 tests/neg/parent-refinement-access.scala create mode 100644 tests/pos/parent-refinement.scala diff --git a/compiler/src/dotty/tools/dotc/core/NamerOps.scala b/compiler/src/dotty/tools/dotc/core/NamerOps.scala index 75a135826785..8d096913e285 100644 --- a/compiler/src/dotty/tools/dotc/core/NamerOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NamerOps.scala @@ -5,6 +5,7 @@ package core import Contexts.*, Symbols.*, Types.*, Flags.*, Scopes.*, Decorators.*, Names.*, NameOps.* import SymDenotations.{LazyType, SymDenotation}, StdNames.nme import TypeApplications.EtaExpansion +import collection.mutable /** Operations that are shared between Namer and TreeUnpickler */ object NamerOps: @@ -18,6 +19,26 @@ object NamerOps: case TypeSymbols(tparams) :: _ => ctor.owner.typeRef.appliedTo(tparams.map(_.typeRef)) case _ => ctor.owner.typeRef + /** Split dependent class refinements off parent type. Add them to `refinements`, + * unless it is null. + */ + extension (tp: Type) + def separateRefinements(cls: ClassSymbol, refinements: mutable.LinkedHashMap[Name, Type] | Null)(using Context): Type = + tp match + case RefinedType(tp1, rname, rinfo) => + try tp1.separateRefinements(cls, refinements) + finally + if refinements != null then + refinements(rname) = refinements.get(rname) match + case Some(tp) => tp & rinfo + case None => rinfo + case tp @ AnnotatedType(tp1, ann) => + tp.derivedAnnotatedType(tp1.separateRefinements(cls, refinements), ann) + case tp: RecType => + tp.parent.substRecThis(tp, cls.thisType).separateRefinements(cls, refinements) + case tp => + tp + /** If isConstructor, make sure it has at least one non-implicit parameter list * This is done by adding a () in front of a leading old style implicit parameter, * or by adding a () as last -- or only -- parameter list if the constructor has diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index 57c0b2217e9d..8c183027bd7b 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -1063,7 +1063,7 @@ class TreeUnpickler(reader: TastyReader, } val parentReader = fork val parents = readParents(withArgs = false)(using parentCtx) - val parentTypes = parents.map(_.tpe.dealias) + val parentTypes = parents.map(_.tpe.dealiasKeepAnnots.separateRefinements(cls, null)) if cls.is(JavaDefined) && parentTypes.exists(_.derivesFrom(defn.JavaAnnotationClass)) then cls.setFlag(JavaAnnotation) val self = diff --git a/compiler/src/dotty/tools/dotc/transform/init/Util.scala b/compiler/src/dotty/tools/dotc/transform/init/Util.scala index 756fd1a0a8e7..e11d0e1e21a5 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Util.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Util.scala @@ -20,6 +20,7 @@ object Util: def typeRefOf(tp: Type)(using Context): TypeRef = tp.dealias.typeConstructor match case tref: TypeRef => tref + case RefinedType(parent, _, _) => typeRefOf(parent) case hklambda: HKTypeLambda => typeRefOf(hklambda.resType) diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 4dffee08bcaa..2f3aa4726519 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -55,11 +55,12 @@ class Namer { typer: Typer => import untpd.* - val TypedAhead : Property.Key[tpd.Tree] = new Property.Key - val ExpandedTree : Property.Key[untpd.Tree] = new Property.Key - val ExportForwarders: Property.Key[List[tpd.MemberDef]] = new Property.Key - val SymOfTree : Property.Key[Symbol] = new Property.Key - val AttachedDeriver : Property.Key[Deriver] = new Property.Key + val TypedAhead : Property.Key[tpd.Tree] = new Property.Key + val ExpandedTree : Property.Key[untpd.Tree] = new Property.Key + val ExportForwarders : Property.Key[List[tpd.MemberDef]] = new Property.Key + val ParentRefinements: Property.Key[List[Symbol]] = new Property.Key + val SymOfTree : Property.Key[Symbol] = new Property.Key + val AttachedDeriver : Property.Key[Deriver] = new Property.Key // was `val Deriver`, but that gave shadowing problems with constructor proxies /** A partial map from unexpanded member and pattern defs and to their expansions. @@ -1502,6 +1503,7 @@ class Namer { typer: Typer => /** The type signature of a ClassDef with given symbol */ override def completeInCreationContext(denot: SymDenotation): Unit = { val parents = impl.parents + val parentRefinements = new mutable.LinkedHashMap[Name, Type] /* The type of a parent constructor. Types constructor arguments * only if parent type contains uninstantiated type parameters. @@ -1556,8 +1558,13 @@ class Namer { typer: Typer => val ptype = parentType(parent)(using completerCtx.superCallContext).dealiasKeepAnnots if (cls.isRefinementClass) ptype else { - val pt = checkClassType(ptype, parent.srcPos, - traitReq = parent ne parents.head, stablePrefixReq = !isJava) + val pt = checkClassType( + if Feature.enabled(modularity) + then ptype.separateRefinements(cls, parentRefinements) + else ptype, + parent.srcPos, + traitReq = parent ne parents.head, + stablePrefixReq = !isJava) if (pt.derivesFrom(cls)) { val addendum = parent match { case Select(qual: Super, _) if Feature.migrateTo3 => @@ -1584,6 +1591,21 @@ class Namer { typer: Typer => } } + /** Enter all parent refinements as public class members, unless a definition + * with the same name already exists in the class. + */ + def enterParentRefinementSyms(refinements: List[(Name, Type)]) = + val refinedSyms = mutable.ListBuffer[Symbol]() + for (name, tp) <- refinements do + if decls.lookupEntry(name) == null then + val flags = tp match + case tp: MethodOrPoly => Method | Synthetic | Deferred + case _ => Synthetic | Deferred + refinedSyms += newSymbol(cls, name, flags, tp, coord = original.rhs.span.startPos).entered + if refinedSyms.nonEmpty then + typr.println(i"parent refinement symbols: ${refinedSyms.toList}") + original.pushAttachment(ParentRefinements, refinedSyms.toList) + /** If `parents` contains references to traits that have supertraits with implicit parameters * add those supertraits in linearization order unless they are already covered by other * parent types. For instance, in @@ -1654,6 +1676,7 @@ class Namer { typer: Typer => cls.invalidateMemberCaches() // we might have checked for a member when parents were not known yet. cls.setNoInitsFlags(parentsKind(parents), untpd.bodyKind(rest)) cls.setStableConstructor() + enterParentRefinementSyms(parentRefinements.toList) processExports(using localCtx) defn.patchStdLibClass(cls) addConstructorProxies(cls) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 0b05bcd078ff..c19306868181 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -40,8 +40,7 @@ import annotation.tailrec import Implicits.* import util.Stats.record import config.Printers.{gadts, typr} -import config.Feature -import config.Feature.{sourceVersion, migrateTo3} +import config.Feature, Feature.{sourceVersion, migrateTo3, modularity} import config.SourceVersion.* import rewrites.Rewrites, Rewrites.patch import staging.StagingLevel @@ -925,10 +924,11 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer tp.exists && !tp.typeSymbol.is(Final) && (!tp.isTopType || tp.isAnyRef) // Object is the only toplevel class that can be instantiated - if (templ1.parents.isEmpty && - isFullyDefined(pt, ForceDegree.flipBottom) && - isSkolemFree(pt) && - isEligible(pt.underlyingClassRef(refinementOK = false))) + if templ1.parents.isEmpty + && isFullyDefined(pt, ForceDegree.flipBottom) + && isSkolemFree(pt) + && isEligible(pt.underlyingClassRef(refinementOK = Feature.enabled(modularity))) + then templ1 = cpy.Template(templ)(parents = untpd.TypeTree(pt) :: Nil) for case parent: RefTree <- templ1.parents do typedAhead(parent, tree => inferTypeParams(typedType(tree), pt)) @@ -2791,6 +2791,19 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } } + /** Add all parent refinement symbols as declarations to this class */ + def addParentRefinements(body: List[Tree])(using Context): List[Tree] = + cdef.getAttachment(ParentRefinements) match + case Some(refinedSyms) => + val refinements = refinedSyms.map: sym => + ( if sym.isType then TypeDef(sym.asType) + else if sym.is(Method) then DefDef(sym.asTerm) + else ValDef(sym.asTerm) + ).withSpan(impl.span.startPos) + body ++ refinements + case None => + body + ensureCorrectSuperClass() completeAnnotations(cdef, cls) val constr1 = typed(constr).asInstanceOf[DefDef] @@ -2811,7 +2824,10 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer cdef.withType(UnspecifiedErrorType) else { val dummy = localDummy(cls, impl) - val body1 = addAccessorDefs(cls, typedStats(impl.body, dummy)(using ctx.inClassContext(self1.symbol))._1) + val body1 = + addParentRefinements( + addAccessorDefs(cls, + typedStats(impl.body, dummy)(using ctx.inClassContext(self1.symbol))._1)) checkNoDoubleDeclaration(cls) val impl1 = cpy.Template(impl)(constr1, parents1, Nil, self1, body1) diff --git a/tests/neg/i0248-inherit-refined.scala b/tests/neg/i0248-inherit-refined.scala index 97b6f5cdab73..f7cd6375afc9 100644 --- a/tests/neg/i0248-inherit-refined.scala +++ b/tests/neg/i0248-inherit-refined.scala @@ -1,10 +1,12 @@ +//> using options -source future -language:experimental.modularity + object test { class A { type T } type X = A { type T = Int } - class B extends X // error + class B extends X // was error, now OK type Y = A & B class C extends Y // error type Z = A | B class D extends Z // error - abstract class E extends ({ val x: Int }) // error + abstract class E extends ({ val x: Int }) // was error, now OK } diff --git a/tests/neg/parent-refinement-access.check b/tests/neg/parent-refinement-access.check new file mode 100644 index 000000000000..5cde9d51558f --- /dev/null +++ b/tests/neg/parent-refinement-access.check @@ -0,0 +1,7 @@ +-- [E164] Declaration Error: tests/neg/parent-refinement-access.scala:6:6 ---------------------------------------------- +6 |trait Year2(private[Year2] val value: Int) extends (Gen { val x: Int }) // error + | ^ + | error overriding value x in trait Year2 of type Int; + | value x in trait Gen of type Any has weaker access privileges; it should be public + | (Note that value x in trait Year2 of type Int is abstract, + | and is therefore overridden by concrete value x in trait Gen of type Any) diff --git a/tests/neg/parent-refinement-access.scala b/tests/neg/parent-refinement-access.scala new file mode 100644 index 000000000000..57d45f4fb201 --- /dev/null +++ b/tests/neg/parent-refinement-access.scala @@ -0,0 +1,6 @@ +//> using options -source future -language:experimental.modularity + +trait Gen: + private[Gen] val x: Any = () + +trait Year2(private[Year2] val value: Int) extends (Gen { val x: Int }) // error diff --git a/tests/neg/parent-refinement.check b/tests/neg/parent-refinement.check index 550430bd35a7..cf9a57bc7821 100644 --- a/tests/neg/parent-refinement.check +++ b/tests/neg/parent-refinement.check @@ -1,4 +1,25 @@ --- Error: tests/neg/parent-refinement.scala:5:2 ------------------------------------------------------------------------ -5 | with Ordered[Year] { // error - | ^^^^ - | end of toplevel definition expected but 'with' found +-- Error: tests/neg/parent-refinement.scala:11:6 ----------------------------------------------------------------------- +11 |class Bar extends IdOf[Int], (X { type Value = String }) // error + | ^^^ + |class Bar cannot be instantiated since it has a member Value with possibly conflicting bounds Int | String <: ... <: Int & String +-- [E007] Type Mismatch Error: tests/neg/parent-refinement.scala:15:17 ------------------------------------------------- +15 | val x: Value = 0 // error + | ^ + | Found: (0 : Int) + | Required: Baz.this.Value + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/parent-refinement.scala:21:6 -------------------------------------------------- +21 | foo(2) // error + | ^ + | Found: (2 : Int) + | Required: Boolean + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/parent-refinement.scala:17:22 ------------------------------------------------- +17 |val x: IdOf[Int] = Baz() // error + | ^^^^^ + | Found: Baz + | Required: IdOf[Int] + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/parent-refinement.scala b/tests/neg/parent-refinement.scala index ca2b88a75fd8..868747faba57 100644 --- a/tests/neg/parent-refinement.scala +++ b/tests/neg/parent-refinement.scala @@ -1,7 +1,21 @@ +//> using options -source future -language:experimental.modularity trait Id { type Value } +trait X { type Value } +type IdOf[T] = Id { type Value = T } + case class Year(value: Int) extends AnyVal - with Id { type Value = Int } - with Ordered[Year] { // error + with (Id { type Value = Int }) + with Ordered[Year] + +class Bar extends IdOf[Int], (X { type Value = String }) // error + +class Baz extends IdOf[Int]: + type Value = String + val x: Value = 0 // error + +val x: IdOf[Int] = Baz() // error -} \ No newline at end of file +object Clash extends ({ def foo(x: Int): Int }): + def foo(x: Boolean): Int = 1 + foo(2) // error diff --git a/tests/pos/parent-refinement.scala b/tests/pos/parent-refinement.scala new file mode 100644 index 000000000000..eaa74228c5d6 --- /dev/null +++ b/tests/pos/parent-refinement.scala @@ -0,0 +1,48 @@ +//> using options -source future -language:experimental.modularity + +class A +class B extends A +class C extends B + +trait Id { type Value } +type IdOf[T] = Id { type Value = T } +trait X { type Value } + +case class Year(value: Int) extends IdOf[Int]: + val x: Value = 2 + +type Between[Lo, Hi] = X { type Value >: Lo <: Hi } + +class Foo() extends IdOf[B], Between[C, A]: + val x: Value = B() + +trait Bar extends IdOf[Int], (X { type Value = String }) + +class Baz extends IdOf[Int]: + type Value = String + val x: Value = "" + +trait Gen: + type T + val x: T + +type IntInst = Gen: + type T = Int + val x: 0 + +trait IntInstTrait extends IntInst + +abstract class IntInstClass extends IntInstTrait, IntInst + +object obj1 extends IntInstTrait: + val x = 0 + +object obj2 extends IntInstClass: + val x = 0 + +def main = + val x: obj1.T = 2 - obj2.x + val y: obj2.T = 2 - obj1.x + + + diff --git a/tests/pos/typeclasses.scala b/tests/pos/typeclasses.scala index 07fe5a31ce5d..2bf7f76f0804 100644 --- a/tests/pos/typeclasses.scala +++ b/tests/pos/typeclasses.scala @@ -1,7 +1,6 @@ -class Common: +//> using options -source future -language:experimental.modularity - // this should go in Predef - infix type at [A <: { type This}, B] = A { type This = B } +class Common: trait Ord: type This @@ -26,41 +25,23 @@ class Common: extension [A](x: This[A]) def flatMap[B](f: A => This[B]): This[B] def map[B](f: A => B) = x.flatMap(f `andThen` pure) + + infix type is[A <: AnyKind, B <: {type This <: AnyKind}] = B { type This = A } + end Common object Instances extends Common: -/* - instance Int: Ord as intOrd with - extension (x: Int) - def compareTo(y: Int) = - if x < y then -1 - else if x > y then +1 - else 0 -*/ - given intOrd: Ord with + given intOrd: (Int is Ord) with type This = Int extension (x: Int) def compareTo(y: Int) = if x < y then -1 else if x > y then +1 else 0 -/* - instance List[T: Ord]: Ord as listOrd with - extension (xs: List[T]) def compareTo(ys: List[T]): Int = (xs, ys) match - case (Nil, Nil) => 0 - case (Nil, _) => -1 - case (_, Nil) => +1 - case (x :: xs1, y :: ys1) => - val fst = x.compareTo(y) - if (fst != 0) fst else xs1.compareTo(ys1) -*/ - // Proposed short syntax: - // given listOrd[T: Ord as ord]: Ord at T with - given listOrd[T](using ord: Ord { type This = T}): Ord with - type This = List[T] + given listOrd[T](using ord: T is Ord): (List[T] is Ord) with extension (xs: List[T]) def compareTo(ys: List[T]): Int = (xs, ys) match case (Nil, Nil) => 0 case (Nil, _) => -1 @@ -70,32 +51,18 @@ object Instances extends Common: if (fst != 0) fst else xs1.compareTo(ys1) end listOrd -/* - instance List: Monad as listMonad with + given listMonad: (List is Monad) with extension [A](xs: List[A]) def flatMap[B](f: A => List[B]): List[B] = xs.flatMap(f) def pure[A](x: A): List[A] = List(x) -*/ - given listMonad: Monad with - type This[A] = List[A] - extension [A](xs: List[A]) def flatMap[B](f: A => List[B]): List[B] = - xs.flatMap(f) - def pure[A](x: A): List[A] = - List(x) -/* - type Reader[Ctx] = X =>> Ctx => X - instance Reader[Ctx: _]: Monad as readerMonad with - extension [A](r: Ctx => A) def flatMap[B](f: A => Ctx => B): Ctx => B = - ctx => f(r(ctx))(ctx) - def pure[A](x: A): Ctx => A = - ctx => x -*/ + type Reader[Ctx] = [X] =>> Ctx => X - given readerMonad[Ctx]: Monad with - type This[X] = Ctx => X + //given [Ctx] => Reader[Ctx] is Monad as readerMonad: + + given readerMonad[Ctx]: (Reader[Ctx] is Monad) with extension [A](r: Ctx => A) def flatMap[B](f: A => Ctx => B): Ctx => B = ctx => f(r(ctx))(ctx) def pure[A](x: A): Ctx => A = @@ -110,29 +77,17 @@ object Instances extends Common: def second = xs.tail.head def third = xs.tail.tail.head - //Proposed short syntax: - //extension [M: Monad as m, A](xss: M[M[A]]) - // def flatten: M[A] = - // xs.flatMap(identity) - extension [M, A](using m: Monad)(xss: m.This[m.This[A]]) def flatten: m.This[A] = xss.flatMap(identity) - // Proposed short syntax: - //def maximum[T: Ord](xs: List[T]: T = - def maximum[T](xs: List[T])(using Ord at T): T = + def maximum[T](xs: List[T])(using T is Ord): T = xs.reduceLeft((x, y) => if (x < y) y else x) - // Proposed short syntax: - // def descending[T: Ord as asc]: Ord at T = new Ord: - def descending[T](using asc: Ord at T): Ord at T = new Ord: - type This = T + def descending[T](using asc: T is Ord): T is Ord = new: extension (x: T) def compareTo(y: T) = asc.compareTo(y)(x) - // Proposed short syntax: - // def minimum[T: Ord](xs: List[T]) = - def minimum[T](xs: List[T])(using Ord at T) = + def minimum[T](xs: List[T])(using T is Ord) = maximum(xs)(using descending) def test(): Unit = @@ -177,10 +132,10 @@ instance Sheep: Animal with override def talk(): Unit = println(s"$name pauses briefly... $noise") */ +import Instances.is // Implement the `Animal` trait for `Sheep`. -given Animal with - type This = Sheep +given (Sheep is Animal) with def apply(name: String) = Sheep(name) extension (self: This) def name: String = self.name From 77464959c1e6ac87964f6f53e2b7fa3090a395e3 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 18 Nov 2023 15:10:34 +0100 Subject: [PATCH 04/63] Allow vals in using clauses of givens --- .../dotty/tools/dotc/parsing/Parsers.scala | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index addd54df9d69..85d39d1f36b4 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -62,7 +62,7 @@ object Parsers { case ExtensionFollow // extension clause, following extension parameter def isClass = // owner is a class - this == Class || this == CaseClass + this == Class || this == CaseClass || this == Given def takesOnlyUsingClauses = // only using clauses allowed for this owner this == Given || this == ExtensionFollow def acceptsVariance = @@ -3330,7 +3330,7 @@ object Parsers { val isAbstractOwner = paramOwner == ParamOwner.Type || paramOwner == ParamOwner.TypeParam val start = in.offset var mods = annotsAsMods() | Param - if paramOwner == ParamOwner.Class || paramOwner == ParamOwner.CaseClass then + if paramOwner.isClass then mods |= PrivateLocal if isIdent(nme.raw.PLUS) && checkVarianceOK() then mods |= Covariant @@ -4058,6 +4058,14 @@ object Parsers { val nameStart = in.offset val name = if isIdent && followingIsGivenSig() then ident() else EmptyTermName + // TODO Change syntax description + def adjustDefParams(paramss: List[ParamClause]): List[ParamClause] = + paramss.nestedMap: param => + if !param.mods.isAllOf(PrivateLocal) then + syntaxError(em"method parameter ${param.name} may not be `a val`", param.span) + param.withMods(param.mods &~ (AccessFlags | ParamAccessor | Mutable) | Param) + .asInstanceOf[List[ParamClause]] + val gdef = val tparams = typeParamClauseOpt(ParamOwner.Given) newLineOpt() @@ -4079,16 +4087,17 @@ object Parsers { mods1 |= Lazy ValDef(name, parents.head, subExpr()) else - DefDef(name, joinParams(tparams, vparamss), parents.head, subExpr()) + DefDef(name, adjustDefParams(joinParams(tparams, vparamss)), parents.head, subExpr()) else if (isStatSep || isStatSeqEnd) && parentsIsType then if name.isEmpty then syntaxError(em"anonymous given cannot be abstract") - DefDef(name, joinParams(tparams, vparamss), parents.head, EmptyTree) + DefDef(name, adjustDefParams(joinParams(tparams, vparamss)), parents.head, EmptyTree) else - val tparams1 = tparams.map(tparam => tparam.withMods(tparam.mods | PrivateLocal)) - val vparamss1 = vparamss.map(_.map(vparam => - vparam.withMods(vparam.mods &~ Param | ParamAccessor | Protected))) - val constr = makeConstructor(tparams1, vparamss1) + val vparamss1 = vparamss.nestedMap: vparam => + if vparam.mods.is(Private) + then vparam.withMods(vparam.mods &~ PrivateLocal | Protected) + else vparam + val constr = makeConstructor(tparams, vparamss1) val templ = if isStatSep || isStatSeqEnd then Template(constr, parents, Nil, EmptyValDef, Nil) else withTemplate(constr, parents) From 062d31a248a5571d92173b205760f2ef6d74ad26 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 18 Nov 2023 15:36:38 +0100 Subject: [PATCH 05/63] Introduce tracked class parameters For a tracked class parameter we add a refinement in the constructor type that the class member is the same as the parameter. E.g. ```scala class C { type T } class D(tracked val x: C) { type T = x.T } ``` This will generate the constructor type: ```scala (x1: C): D { val x: x1.type } ``` Without `tracked` the refinement would not be added. This can solve several problems with dependent class types where previously we lost track of type dependencies. --- .../src/dotty/tools/dotc/ast/Desugar.scala | 35 +++- compiler/src/dotty/tools/dotc/ast/untpd.scala | 2 + .../src/dotty/tools/dotc/core/Flags.scala | 9 +- .../src/dotty/tools/dotc/core/NamerOps.scala | 11 +- .../dotc/core/PatternTypeConstrainer.scala | 9 +- .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../tools/dotc/core/SymDenotations.scala | 8 +- .../src/dotty/tools/dotc/core/TypeUtils.scala | 14 +- .../tools/dotc/core/tasty/TreePickler.scala | 1 + .../tools/dotc/core/tasty/TreeUnpickler.scala | 1 + .../dotty/tools/dotc/parsing/Parsers.scala | 17 +- .../tools/dotc/printing/PlainPrinter.scala | 2 +- .../dotty/tools/dotc/printing/Printer.scala | 5 +- .../tools/dotc/transform/PostTyper.scala | 16 +- .../src/dotty/tools/dotc/typer/Checking.scala | 13 +- .../src/dotty/tools/dotc/typer/Namer.scala | 8 +- .../dotty/tools/dotc/typer/RefChecks.scala | 17 +- .../src/dotty/tools/dotc/typer/Typer.scala | 2 +- .../test/dotc/pos-test-pickling.blacklist | 2 + docs/_docs/internals/syntax.md | 2 +- .../reference/experimental/modularity.md | 191 ++++++++++++++++++ docs/sidebar.yml | 1 + .../runtime/stdLibPatches/language.scala | 1 + tasty/src/dotty/tools/tasty/TastyFormat.scala | 5 +- tests/neg/i3964.scala | 12 ++ tests/neg/tracked.check | 50 +++++ tests/neg/tracked.scala | 20 ++ tests/neg/tracked2.scala | 1 + tests/new/tracked-mixin-traits.scala | 16 ++ tests/pos/depclass-1.scala | 19 ++ tests/pos/i3920.scala | 32 +++ tests/pos/i3964.scala | 32 +++ tests/pos/i3964a/Defs_1.scala | 18 ++ tests/pos/i3964a/Uses_2.scala | 16 ++ tests/pos/parsercombinators-expanded.scala | 64 ++++++ tests/pos/parsercombinators-givens-2.scala | 52 +++++ tests/pos/parsercombinators-givens.scala | 54 +++++ tests/run/i3920.scala | 26 +++ 38 files changed, 727 insertions(+), 58 deletions(-) create mode 100644 docs/_docs/reference/experimental/modularity.md create mode 100644 tests/neg/i3964.scala create mode 100644 tests/neg/tracked.check create mode 100644 tests/neg/tracked.scala create mode 100644 tests/neg/tracked2.scala create mode 100644 tests/new/tracked-mixin-traits.scala create mode 100644 tests/pos/depclass-1.scala create mode 100644 tests/pos/i3920.scala create mode 100644 tests/pos/i3964.scala create mode 100644 tests/pos/i3964a/Defs_1.scala create mode 100644 tests/pos/i3964a/Uses_2.scala create mode 100644 tests/pos/parsercombinators-expanded.scala create mode 100644 tests/pos/parsercombinators-givens-2.scala create mode 100644 tests/pos/parsercombinators-givens.scala create mode 100644 tests/run/i3920.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 6ed05976a19e..e4bddcd33ede 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -429,13 +429,13 @@ object desugar { private def toDefParam(tparam: TypeDef, keepAnnotations: Boolean): TypeDef = { var mods = tparam.rawMods if (!keepAnnotations) mods = mods.withAnnotations(Nil) - tparam.withMods(mods & (EmptyFlags | Sealed) | Param) + tparam.withMods(mods & EmptyFlags | Param) } private def toDefParam(vparam: ValDef, keepAnnotations: Boolean, keepDefault: Boolean): ValDef = { var mods = vparam.rawMods if (!keepAnnotations) mods = mods.withAnnotations(Nil) val hasDefault = if keepDefault then HasDefault else EmptyFlags - vparam.withMods(mods & (GivenOrImplicit | Erased | hasDefault) | Param) + vparam.withMods(mods & (GivenOrImplicit | Erased | hasDefault | Tracked) | Param) } def mkApply(fn: Tree, paramss: List[ParamClause])(using Context): Tree = @@ -521,7 +521,7 @@ object desugar { // but not on the constructor parameters. The reverse is true for // annotations on class _value_ parameters. val constrTparams = impliedTparams.map(toDefParam(_, keepAnnotations = false)) - val constrVparamss = + def defVparamss = if (originalVparamss.isEmpty) { // ensure parameter list is non-empty if (isCaseClass) report.error(CaseClassMissingParamList(cdef), namePos) @@ -532,6 +532,10 @@ object desugar { ListOfNil } else originalVparamss.nestedMap(toDefParam(_, keepAnnotations = true, keepDefault = true)) + val constrVparamss = defVparamss + // defVparamss also needed as separate tree nodes in implicitWrappers below. + // Need to be separate because they are `watch`ed in addParamRefinements. + // See parsercombinators-givens.scala for a test case. val derivedTparams = constrTparams.zipWithConserve(impliedTparams)((tparam, impliedParam) => derivedTypeParam(tparam).withAnnotations(impliedParam.mods.annotations)) @@ -609,6 +613,11 @@ object desugar { case _ => false } + def isRepeated(tree: Tree): Boolean = stripByNameType(tree) match { + case PostfixOp(_, Ident(tpnme.raw.STAR)) => true + case _ => false + } + def appliedRef(tycon: Tree, tparams: List[TypeDef] = constrTparams, widenHK: Boolean = false) = { val targs = for (tparam <- tparams) yield { val targ = refOfDef(tparam) @@ -625,10 +634,13 @@ object desugar { appliedTypeTree(tycon, targs) } - def isRepeated(tree: Tree): Boolean = stripByNameType(tree) match { - case PostfixOp(_, Ident(tpnme.raw.STAR)) => true - case _ => false - } + def addParamRefinements(core: Tree, paramss: List[List[ValDef]]): Tree = + val refinements = + for params <- paramss; param <- params; if param.mods.is(Tracked) yield + ValDef(param.name, SingletonTypeTree(TermRefTree().watching(param)), EmptyTree) + .withSpan(param.span) + if refinements.isEmpty then core + else RefinedTypeTree(core, refinements).showing(i"refined result: $result", Printers.desugar) // a reference to the class type bound by `cdef`, with type parameters coming from the constructor val classTypeRef = appliedRef(classTycon) @@ -850,18 +862,17 @@ object desugar { Nil } else { - val defParamss = constrVparamss match { + val defParamss = defVparamss match case Nil :: paramss => paramss // drop leading () that got inserted by class // TODO: drop this once we do not silently insert empty class parameters anymore case paramss => paramss - } val finalFlag = if ctx.settings.YcompileScala2Library.value then EmptyFlags else Final // implicit wrapper is typechecked in same scope as constructor, so // we can reuse the constructor parameters; no derived params are needed. DefDef( className.toTermName, joinParams(constrTparams, defParamss), - classTypeRef, creatorExpr) + addParamRefinements(classTypeRef, defParamss), creatorExpr) .withMods(companionMods | mods.flags.toTermFlags & (GivenOrImplicit | Inline) | finalFlag) .withSpan(cdef.span) :: Nil } @@ -890,7 +901,9 @@ object desugar { } if mods.isAllOf(Given | Inline | Transparent) then report.error("inline given instances cannot be trasparent", cdef) - val classMods = if mods.is(Given) then mods &~ (Inline | Transparent) | Synthetic else mods + var classMods = if mods.is(Given) then mods &~ (Inline | Transparent) | Synthetic else mods + if vparamAccessors.exists(_.mods.is(Tracked)) then + classMods |= Dependent cpy.TypeDef(cdef: TypeDef)( name = className, rhs = cpy.Template(impl)(constr, parents1, clsDerived, self1, diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 08f3db4981ff..def0ca4d755c 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -230,6 +230,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case class Infix()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Infix) + case class Tracked()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Tracked) + /** Used under pureFunctions to mark impure function types `A => B` in `FunctionWithMods` */ case class Impure()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Impure) } diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index 6bbdedcb006b..0764d4149468 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -377,6 +377,9 @@ object Flags { /** Symbol cannot be found as a member during typer */ val (Invisible @ _, _, _) = newFlags(45, "") + /** Tracked modifier for class parameter / a class with some tracked parameters */ + val (Tracked @ _, _, Dependent @ _) = newFlags(46, "tracked") + // ------------ Flags following this one are not pickled ---------------------------------- /** Symbol is not a member of its owner */ @@ -452,7 +455,7 @@ object Flags { CommonSourceModifierFlags.toTypeFlags | Abstract | Sealed | Opaque | Open val TermSourceModifierFlags: FlagSet = - CommonSourceModifierFlags.toTermFlags | Inline | AbsOverride | Lazy + CommonSourceModifierFlags.toTermFlags | Inline | AbsOverride | Lazy | Tracked /** Flags representing modifiers that can appear in trees */ val ModifierFlags: FlagSet = @@ -466,7 +469,7 @@ object Flags { val FromStartFlags: FlagSet = commonFlags( Module, Package, Deferred, Method, Case, Enum, Param, ParamAccessor, Scala2SpecialFlags, MutableOrOpen, Opaque, Touched, JavaStatic, - OuterOrCovariant, LabelOrContravariant, CaseAccessor, + OuterOrCovariant, LabelOrContravariant, CaseAccessor, Tracked, Extension, NonMember, Implicit, Given, Permanent, Synthetic, Exported, SuperParamAliasOrScala2x, Inline, Macro, ConstructorProxy, Invisible) @@ -477,7 +480,7 @@ object Flags { */ val AfterLoadFlags: FlagSet = commonFlags( FromStartFlags, AccessFlags, Final, AccessorOrSealed, - Abstract, LazyOrTrait, SelfName, JavaDefined, JavaAnnotation, Transparent) + Abstract, LazyOrTrait, SelfName, JavaDefined, JavaAnnotation, Transparent, Tracked) /** A value that's unstable unless complemented with a Stable flag */ val UnstableValueFlags: FlagSet = Mutable | Method diff --git a/compiler/src/dotty/tools/dotc/core/NamerOps.scala b/compiler/src/dotty/tools/dotc/core/NamerOps.scala index 8d096913e285..b791088914ea 100644 --- a/compiler/src/dotty/tools/dotc/core/NamerOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NamerOps.scala @@ -15,9 +15,14 @@ object NamerOps: * @param ctor the constructor */ def effectiveResultType(ctor: Symbol, paramss: List[List[Symbol]])(using Context): Type = - paramss match - case TypeSymbols(tparams) :: _ => ctor.owner.typeRef.appliedTo(tparams.map(_.typeRef)) - case _ => ctor.owner.typeRef + val (resType, termParamss) = paramss match + case TypeSymbols(tparams) :: rest => + (ctor.owner.typeRef.appliedTo(tparams.map(_.typeRef)), rest) + case _ => + (ctor.owner.typeRef, paramss) + termParamss.flatten.foldLeft(resType): (rt, param) => + if param.is(Tracked) then RefinedType(rt, param.name, param.termRef) + else rt /** Split dependent class refinements off parent type. Add them to `refinements`, * unless it is null. diff --git a/compiler/src/dotty/tools/dotc/core/PatternTypeConstrainer.scala b/compiler/src/dotty/tools/dotc/core/PatternTypeConstrainer.scala index 38f8e19e2737..9273c2ec59b3 100644 --- a/compiler/src/dotty/tools/dotc/core/PatternTypeConstrainer.scala +++ b/compiler/src/dotty/tools/dotc/core/PatternTypeConstrainer.scala @@ -88,11 +88,6 @@ trait PatternTypeConstrainer { self: TypeComparer => } } - def stripRefinement(tp: Type): Type = tp match { - case tp: RefinedOrRecType => stripRefinement(tp.parent) - case tp => tp - } - def tryConstrainSimplePatternType(pat: Type, scrut: Type) = { val patCls = pat.classSymbol val scrCls = scrut.classSymbol @@ -181,14 +176,14 @@ trait PatternTypeConstrainer { self: TypeComparer => case AndType(scrut1, scrut2) => constrainPatternType(pat, scrut1) && constrainPatternType(pat, scrut2) case scrut: RefinedOrRecType => - constrainPatternType(pat, stripRefinement(scrut)) + constrainPatternType(pat, scrut.stripRefinement) case scrut => dealiasDropNonmoduleRefs(pat) match { case OrType(pat1, pat2) => either(constrainPatternType(pat1, scrut), constrainPatternType(pat2, scrut)) case AndType(pat1, pat2) => constrainPatternType(pat1, scrut) && constrainPatternType(pat2, scrut) case pat: RefinedOrRecType => - constrainPatternType(stripRefinement(pat), scrut) + constrainPatternType(pat.stripRefinement, scrut) case pat => tryConstrainSimplePatternType(pat, scrut) || classesMayBeCompatible && constrainUpcasted(scrut) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 9772199678d7..1313eceb01bf 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -625,6 +625,7 @@ object StdNames { val toString_ : N = "toString" val toTypeConstructor: N = "toTypeConstructor" val tpe : N = "tpe" + val tracked: N = "tracked" val transparent : N = "transparent" val tree : N = "tree" val true_ : N = "true" diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 5304c9efadc0..55f7b3f4b22c 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -1190,13 +1190,17 @@ object SymDenotations { final def isExtensibleClass(using Context): Boolean = isClass && !isOneOf(FinalOrModuleClass) && !isAnonymousClass - /** A symbol is effectively final if it cannot be overridden in a subclass */ + /** A symbol is effectively final if it cannot be overridden */ final def isEffectivelyFinal(using Context): Boolean = isOneOf(EffectivelyFinalFlags) || is(Inline, butNot = Deferred) || is(JavaDefinedVal, butNot = Method) || isConstructor - || !owner.isExtensibleClass + || !owner.isExtensibleClass && !is(Deferred) + // Deferred symbols can arise through parent refinements. + // For them, the overriding relationship reverses anyway, so + // being in a final class does not mean the symbol cannot be + // implemented concretely in a superclass. /** A class is effectively sealed if has the `final` or `sealed` modifier, or it * is defined in Scala 3 and is neither abstract nor open. diff --git a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala index c76b5117dc89..7c4acaebb18b 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala @@ -6,11 +6,11 @@ import TypeErasure.ErasedValueType import Types.*, Contexts.*, Symbols.*, Flags.*, Decorators.* import Names.Name -class TypeUtils { +class TypeUtils: /** A decorator that provides methods on types * that are needed in the transformer pipeline. */ - extension (self: Type) { + extension (self: Type) def isErasedValueType(using Context): Boolean = self.isInstanceOf[ErasedValueType] @@ -150,5 +150,11 @@ class TypeUtils { case _ => val cls = self.underlyingClassRef(refinementOK = false).typeSymbol cls.isTransparentClass && (!traitOnly || cls.is(Trait)) - } -} + + /** Strip all outer refinements off this type */ + def stripRefinement: Type = self match + case self: RefinedOrRecType => self.parent.stripRefinement + case seld => self + +end TypeUtils + diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala index 7d2d95aa9601..47cb0ba3d4d6 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala @@ -807,6 +807,7 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) { if (flags.is(Exported)) writeModTag(EXPORTED) if (flags.is(Given)) writeModTag(GIVEN) if (flags.is(Implicit)) writeModTag(IMPLICIT) + if (flags.is(Tracked)) writeModTag(TRACKED) if (isTerm) { if (flags.is(Lazy, butNot = Module)) writeModTag(LAZY) if (flags.is(AbsOverride)) { writeModTag(ABSTRACT); writeModTag(OVERRIDE) } diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index 8c183027bd7b..66a761835acd 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -751,6 +751,7 @@ class TreeUnpickler(reader: TastyReader, case INVISIBLE => addFlag(Invisible) case TRANSPARENT => addFlag(Transparent) case INFIX => addFlag(Infix) + case TRACKED => addFlag(Tracked) case PRIVATEqualified => readByte() privateWithin = readWithin diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 85d39d1f36b4..57a340453177 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -3147,6 +3147,7 @@ object Parsers { case nme.open => Mod.Open() case nme.transparent => Mod.Transparent() case nme.infix => Mod.Infix() + case nme.tracked => Mod.Tracked() } } @@ -3213,7 +3214,8 @@ object Parsers { * | AccessModifier * | override * | opaque - * LocalModifier ::= abstract | final | sealed | open | implicit | lazy | inline | transparent | infix | erased + * LocalModifier ::= abstract | final | sealed | open | implicit | lazy | erased | + * inline | transparent */ def modifiers(allowed: BitSet = modifierTokens, start: Modifiers = Modifiers()): Modifiers = { @tailrec @@ -3366,8 +3368,8 @@ object Parsers { /** ClsTermParamClause ::= ‘(’ ClsParams ‘)’ | UsingClsTermParamClause * UsingClsTermParamClause::= ‘(’ ‘using’ [‘erased’] (ClsParams | ContextTypes) ‘)’ * ClsParams ::= ClsParam {‘,’ ClsParam} - * ClsParam ::= {Annotation} [{Modifier} (‘val’ | ‘var’)] Param - * + * ClsParam ::= {Annotation} + * [{Modifier | ‘tracked’} (‘val’ | ‘var’)] Param * TypelessClause ::= DefTermParamClause * | UsingParamClause * @@ -3403,6 +3405,8 @@ object Parsers { if isErasedKw then mods = addModifier(mods) if paramOwner.isClass then + if isIdent(nme.tracked) && in.featureEnabled(Feature.modularity) && !in.lookahead.isColon then + mods = addModifier(mods) mods = addFlag(modifiers(start = mods), ParamAccessor) mods = if in.token == VAL then @@ -3474,7 +3478,8 @@ object Parsers { val isParams = !impliedMods.is(Given) || startParamTokens.contains(in.token) - || isIdent && (in.name == nme.inline || in.lookahead.isColon) + || isIdent + && (in.name == nme.inline || in.name == nme.tracked || in.lookahead.isColon) (mods, isParams) (if isParams then commaSeparated(() => param()) else contextTypes(paramOwner, numLeadParams, impliedMods)) match { @@ -4062,8 +4067,8 @@ object Parsers { def adjustDefParams(paramss: List[ParamClause]): List[ParamClause] = paramss.nestedMap: param => if !param.mods.isAllOf(PrivateLocal) then - syntaxError(em"method parameter ${param.name} may not be `a val`", param.span) - param.withMods(param.mods &~ (AccessFlags | ParamAccessor | Mutable) | Param) + syntaxError(em"method parameter ${param.name} may not be a `val`", param.span) + param.withMods(param.mods &~ (AccessFlags | ParamAccessor | Tracked | Mutable) | Param) .asInstanceOf[List[ParamClause]] val gdef = diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index ac7b4ef39604..ff6419c48801 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -112,7 +112,7 @@ class PlainPrinter(_ctx: Context) extends Printer { protected def refinementNameString(tp: RefinedType): String = nameString(tp.refinedName) /** String representation of a refinement */ - protected def toTextRefinement(rt: RefinedType): Text = + def toTextRefinement(rt: RefinedType): Text = val keyword = rt.refinedInfo match { case _: ExprType | _: MethodOrPoly => "def " case _: TypeBounds => "type " diff --git a/compiler/src/dotty/tools/dotc/printing/Printer.scala b/compiler/src/dotty/tools/dotc/printing/Printer.scala index 8687925ed5fb..297dc31ea94a 100644 --- a/compiler/src/dotty/tools/dotc/printing/Printer.scala +++ b/compiler/src/dotty/tools/dotc/printing/Printer.scala @@ -4,7 +4,7 @@ package printing import core.* import Texts.*, ast.Trees.* -import Types.{Type, SingletonType, LambdaParam, NamedType}, +import Types.{Type, SingletonType, LambdaParam, NamedType, RefinedType}, Symbols.Symbol, Scopes.Scope, Constants.Constant, Names.Name, Denotations._, Annotations.Annotation, Contexts.Context import typer.Implicits.* @@ -104,6 +104,9 @@ abstract class Printer { /** Textual representation of a prefix of some reference, ending in `.` or `#` */ def toTextPrefixOf(tp: NamedType): Text + /** textual representation of a refinement, with no enclosing {...} */ + def toTextRefinement(rt: RefinedType): Text + /** Textual representation of a reference in a capture set */ def toTextCaptureRef(tp: Type): Text diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 3bcec80b5b10..18222dd2f66b 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -336,11 +336,15 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => case Select(nu: New, nme.CONSTRUCTOR) if isCheckable(nu) => // need to check instantiability here, because the type of the New itself // might be a type constructor. - ctx.typer.checkClassType(tree.tpe, tree.srcPos, traitReq = false, stablePrefixReq = true) + def checkClassType(tpe: Type, stablePrefixReq: Boolean) = + ctx.typer.checkClassType(tpe, tree.srcPos, + traitReq = false, stablePrefixReq = stablePrefixReq, + refinementOK = Feature.enabled(Feature.modularity)) + checkClassType(tree.tpe, true) if !nu.tpe.isLambdaSub then // Check the constructor type as well; it could be an illegal singleton type // which would not be reflected as `tree.tpe` - ctx.typer.checkClassType(nu.tpe, tree.srcPos, traitReq = false, stablePrefixReq = false) + checkClassType(nu.tpe, false) Checking.checkInstantiable(tree.tpe, nu.tpe, nu.srcPos) withNoCheckNews(nu :: Nil)(app1) case _ => @@ -416,8 +420,12 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => // Constructor parameters are in scope when typing a parent. // While they can safely appear in a parent tree, to preserve // soundness we need to ensure they don't appear in a parent - // type (#16270). - val illegalRefs = parent.tpe.namedPartsWith(p => p.symbol.is(ParamAccessor) && (p.symbol.owner eq sym)) + // type (#16270). We can strip any refinement of a parent type since + // these refinements are split off from the parent type constructor + // application `parent` in Namer and don't show up as parent types + // of the class. + val illegalRefs = parent.tpe.stripRefinement.namedPartsWith: + p => p.symbol.is(ParamAccessor) && (p.symbol.owner eq sym) if illegalRefs.nonEmpty then report.error( em"The type of a class parent cannot refer to constructor parameters, but ${parent.tpe} refers to ${illegalRefs.map(_.name.show).mkString(",")}", parent.srcPos) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 56f67574a72d..1bd0d4035758 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -33,8 +33,7 @@ import Applications.unapplyArgs import Inferencing.isFullyDefined import transform.patmat.SpaceEngine.{isIrrefutable, isIrrefutableQuotePattern} import transform.ValueClasses.underlyingOfValueClass -import config.Feature -import config.Feature.sourceVersion +import config.Feature, Feature.{sourceVersion, modularity} import config.SourceVersion.* import config.MigrationVersion import printing.Formatting.hlAsKeyword @@ -197,7 +196,7 @@ object Checking { * and that the instance conforms to the self type of the created class. */ def checkInstantiable(tp: Type, srcTp: Type, pos: SrcPos)(using Context): Unit = - tp.underlyingClassRef(refinementOK = false) match + tp.underlyingClassRef(refinementOK = Feature.enabled(modularity)) match case tref: TypeRef => val cls = tref.symbol if (cls.isOneOf(AbstractOrTrait)) { @@ -605,6 +604,7 @@ object Checking { // The issue with `erased inline` is that the erased semantics get lost // as the code is inlined and the reference is removed before the erased usage check. checkCombination(Erased, Inline) + checkNoConflict(Tracked, Mutable, em"mutable variables may not be `tracked`") checkNoConflict(Lazy, ParamAccessor, em"parameter may not be `lazy`") } @@ -1058,8 +1058,8 @@ trait Checking { * check that class prefix is stable. * @return `tp` itself if it is a class or trait ref, ObjectType if not. */ - def checkClassType(tp: Type, pos: SrcPos, traitReq: Boolean, stablePrefixReq: Boolean)(using Context): Type = - tp.underlyingClassRef(refinementOK = false) match { + def checkClassType(tp: Type, pos: SrcPos, traitReq: Boolean, stablePrefixReq: Boolean, refinementOK: Boolean = false)(using Context): Type = + tp.underlyingClassRef(refinementOK) match case tref: TypeRef => if (traitReq && !tref.symbol.is(Trait)) report.error(TraitIsExpected(tref.symbol), pos) if (stablePrefixReq && ctx.phase <= refchecksPhase) checkStable(tref.prefix, pos, "class prefix") @@ -1067,7 +1067,6 @@ trait Checking { case _ => report.error(NotClassType(tp), pos) defn.ObjectType - } /** If `sym` is an old-style implicit conversion, check that implicit conversions are enabled. * @pre sym.is(GivenOrImplicit) @@ -1617,7 +1616,7 @@ trait NoChecking extends ReChecking { override def checkNonCyclic(sym: Symbol, info: TypeBounds, reportErrors: Boolean)(using Context): Type = info override def checkNonCyclicInherited(joint: Type, parents: List[Type], decls: Scope, pos: SrcPos)(using Context): Unit = () override def checkStable(tp: Type, pos: SrcPos, kind: String)(using Context): Unit = () - override def checkClassType(tp: Type, pos: SrcPos, traitReq: Boolean, stablePrefixReq: Boolean)(using Context): Type = tp + override def checkClassType(tp: Type, pos: SrcPos, traitReq: Boolean, stablePrefixReq: Boolean, refinementOK: Boolean)(using Context): Type = tp override def checkImplicitConversionDefOK(sym: Symbol)(using Context): Unit = () override def checkImplicitConversionUseOK(tree: Tree, expected: Type)(using Context): Unit = () override def checkFeasibleParent(tp: Type, pos: SrcPos, where: => String = "")(using Context): Type = tp diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 2f3aa4726519..34960cafcf3b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -1517,8 +1517,9 @@ class Namer { typer: Typer => core match case Select(New(tpt), nme.CONSTRUCTOR) => val targs1 = targs map (typedAheadType(_)) - val ptype = typedAheadType(tpt).tpe appliedTo targs1.tpes - if (ptype.typeParams.isEmpty) ptype + val ptype = typedAheadType(tpt).tpe.appliedTo(targs1.tpes) + if ptype.typeParams.isEmpty && !ptype.dealias.typeSymbol.is(Dependent) then + ptype else if (denot.is(ModuleClass) && denot.sourceModule.isOneOf(GivenOrImplicit)) missingType(denot.symbol, "parent ")(using creationContext) @@ -1599,7 +1600,8 @@ class Namer { typer: Typer => for (name, tp) <- refinements do if decls.lookupEntry(name) == null then val flags = tp match - case tp: MethodOrPoly => Method | Synthetic | Deferred + case tp: MethodOrPoly => Method | Synthetic | Deferred | Tracked + case _ if name.isTermName => Synthetic | Deferred | Tracked case _ => Synthetic | Deferred refinedSyms += newSymbol(cls, name, flags, tp, coord = original.rhs.span.startPos).entered if refinedSyms.nonEmpty then diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index 173d5e6b1f7e..4834fc37d29b 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -610,8 +610,13 @@ object RefChecks { overrideError("is not inline, cannot implement an inline method") else if (other.isScala2Macro && !member.isScala2Macro) // (1.11) overrideError("cannot be used here - only Scala-2 macros can override Scala-2 macros") - else if (!compatTypes(memberTp(self), otherTp(self)) && - !compatTypes(memberTp(upwardsSelf), otherTp(upwardsSelf))) + else if !compatTypes(memberTp(self), otherTp(self)) + && !compatTypes(memberTp(upwardsSelf), otherTp(upwardsSelf)) + && !member.is(Tracked) + // Tracked members need to be excluded since they are abstract type members with + // singleton types. Concrete overrides usually have a wider type. + // TODO: Should we exclude all refinements inherited from parents? + then overrideError("has incompatible type", compareTypes = true) else if (member.targetName != other.targetName) if (other.targetName != other.name) @@ -620,7 +625,9 @@ object RefChecks { overrideError("cannot have a @targetName annotation since external names would be different") else if intoOccurrences(memberTp(self)) != intoOccurrences(otherTp(self)) then overrideError("has different occurrences of `into` modifiers", compareTypes = true) - else if other.is(ParamAccessor) && !isInheritedAccessor(member, other) then // (1.12) + else if other.is(ParamAccessor) && !isInheritedAccessor(member, other) + && !member.is(Tracked) + then // (1.12) report.errorOrMigrationWarning( em"cannot override val parameter ${other.showLocated}", member.srcPos, @@ -670,6 +677,10 @@ object RefChecks { mbr.isType || mbr.isSuperAccessor // not yet synthesized || mbr.is(JavaDefined) && hasJavaErasedOverriding(mbr) + || mbr.is(Tracked) + // Tracked members correspond to existing val parameters, so they don't + // count as deferred. The val parameter could not implement the tracked + // refinement since it usually has a wider type. def isImplemented(mbr: Symbol) = val mbrDenot = mbr.asSeenFrom(clazz.thisType) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index c19306868181..b601def0398e 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -4358,7 +4358,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer cpy.Ident(qual)(qual.symbol.name.sourceModuleName.toTypeName) case _ => errorTree(tree, em"cannot convert from $tree to an instance creation expression") - val tycon = ctorResultType.underlyingClassRef(refinementOK = false) + val tycon = ctorResultType.underlyingClassRef(refinementOK = true) typed( untpd.Select( untpd.New(untpd.TypedSplice(tpt.withType(tycon))), diff --git a/compiler/test/dotc/pos-test-pickling.blacklist b/compiler/test/dotc/pos-test-pickling.blacklist index 3785f8fa6e06..2b28cc078a8c 100644 --- a/compiler/test/dotc/pos-test-pickling.blacklist +++ b/compiler/test/dotc/pos-test-pickling.blacklist @@ -118,3 +118,5 @@ i7445b.scala # more aggresive reduce projection makes a difference i15525.scala +# alias types at different levels of dereferencing +parsercombinators-givens.scala \ No newline at end of file diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index 10f068e53c7f..d2a2bd6de80a 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -365,7 +365,7 @@ ClsParamClause ::= [nl] ‘(’ ClsParams ‘)’ | [nl] ‘(’ ‘using’ (ClsParams | FunArgTypes) ‘)’ ClsParams ::= ClsParam {‘,’ ClsParam} ClsParam ::= {Annotation} ValDef(mods, id, tpe, expr) -- point of mods on val/var - [{Modifier} (‘val’ | ‘var’)] Param + [{Modifier | ‘tracked’} (‘val’ | ‘var’)] Param DefParamClauses ::= DefParamClause { DefParamClause } -- and two DefTypeParamClause cannot be adjacent DefParamClause ::= DefTypeParamClause diff --git a/docs/_docs/reference/experimental/modularity.md b/docs/_docs/reference/experimental/modularity.md new file mode 100644 index 000000000000..ccbd141bad4e --- /dev/null +++ b/docs/_docs/reference/experimental/modularity.md @@ -0,0 +1,191 @@ +--- +layout: doc-page +title: "Modularity Improvements" +nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/modularity.html +--- + +# Modularity Improvements + +Martin Odersky, 7.1.2024 + +Scala is a language in the SML tradition, in the sense that it has +abstract and alias types as members of classes. This leads to a simple dependently +typed system, where dependencies in types are on paths instead of full terms. + +So far, some key ingredients were lacking which meant that module composition with functors is harder in Scala than in SML. In particular, one often needs to resort the infamous `Aux` pattern that lifts type members into type parameters so that they can be tracked across class instantiations. This makes modular, dependently typed programs +much harder to write and read, and makes such programming only accessible to experts. + +In this note I propose some small changes to Scala's dependent typing that makes +modular programming much more straightforward. + +The suggested improvements have been implemented and are available +in source version `future` if the additional experimental language import `modularity` is present. For instance, using + +``` + scala compile -source future -language experimental.modularity +``` + +## Tracked Parameters + +Scala is dependently typed for functions, but unfortunately not for classes. +For instance, consider the following definitions: + +```scala + class C: + type T + ... + + def f(x: C): x.T = ... + + val y: C { type T = Int } +``` +Then `f(y)` would have type `Int`, since the compiler will substitute the +concrete parameter reference `y` for the formal parameter `x` in the result +type of `f`. + +However, if we use a class `F` instead of a method `f`, things go wrong. + +```scala + class F(val x: C): + val result: x.T = ... +``` +Now `F(y).result` would not have type `Int` but instead the rather less useful type `?1.T` where `?1` is a so-called skolem constant of type `C` (a skolem represents an unknown value). + +This shortcoming means that classes cannot really be used for advanced +modularity constructs that rely on dependent typing. + +**Proposal:** Introduce a `tracked` modifier that can be added to +a `val` parameter of a class or trait. For every tracked class parameter of a class `C`, add a refinement in the constructor type of `C` that the class member is the same as the parameter. + +**Example:** In the setting above, assume `F` is instead declared like this: +```scala + class F(tracked val x: C): + val result: x.T = ... +``` +Then the constructor `F` would get roughly the following type: +```scala + F(x1: C): F { val x: x1.type } +``` +_Aside:_ More precisely, both parameter and refinement would apply to the same name `x` but the refinement still refers to the parameter. We unfortunately can't express that in source, however, so we chose the new name `x1` for the parameter. + +With the new constructor type, the expression `F(y).result` would now have the type `Int`, as hoped for. The reasoning to get there is as follows: + + - The result of the constructor `F(y)` has type `F { val x: y.type }` by + the standard typing for dependent functions. + - The type of `result` inside `F` is `x.T`. + - Hence, the type of `result` as a member of `F { val x: y.type }` is `y.T`, which is equal to `Int`. + +The addition of tracked parameters makes classes suitable as a fundamental modularity construct supporting dependent typing. Here is an example, taken from issue #3920: + +```scala +trait Ordering: + type T + def compare(t1:T, t2: T): Int + +class SetFunctor(tracked val ord: Ordering): + type Set = List[ord.T] + + def empty: Set = Nil + + extension (s: Set) + def add(x: ord.T): Set = x :: remove(x) + def remove(x: ord.T): Set = s.filter(e => ord.compare(x, e) != 0) + def contains(x: ord.T): Boolean = s.exists(e => ord.compare(x, e) == 0) + +object intOrdering extends Ordering: + type T = Int + def compare(t1: T, t2: T): Int = t1 - t2 + +val IntSet = new SetFunctor(intOrdering) + +@main def Test = + import IntSet.* + val set = IntSet.empty.add(6).add(8).add(23) + assert(!set.contains(7)) + assert(set.contains(8)) +``` +This works as it should now. Without the addition of `tracked` to the +parameter of `SetFunctor` typechecking would immediately lose track of +the element type `T` after an `add`, and would therefore fail. + +**Syntax Change** + +``` +ClsParam ::= {Annotation} [{Modifier | ‘tracked’} (‘val’ | ‘var’) | ‘inline’] Param +``` + +The (soft) `tracked` modifier is only allowed for `val` parameters of classes. + +**Discussion** + +Since `tracked` is so useful, why not assume it by default? First, `tracked` makes sense only for `val` parameters. If a class parameter is not also a field declared using `val` then there's nothing to refine in the constructor. +But making all `val` parameters tracked by default would also be a backwards +incompatible change. For instance, the following code would break: + +```scala +case class Foo(x: Int) +var foo = Foo(1) +if someCondition then foo = Foo(2) +``` +if we assume `tracked` for parameter `x` (which is implicitly a `val`), +then `foo` would get inferred type `Foo { val x: 1 }`, so it could not +be reassigned to a value of type `Foo { val x: 2 }`. + +Another approach might be to assume `tracked` for a `val` parameter `x` +only if the class refers to a type member of `x`. But it turns out that this +scheme is unimplementable since it would quickly lead to cyclic references +in recursive class graphs. So an explicit `tracked` looks like the best feasible option. + +## Allow Class Parents to be Refined Types + +Since `tracked` parameters create refinements in constructor types, +it is now possible that a class has a parent that is a refined type. +Previously such types were not permitted, since we were not quite sure how to handle them. But with tracked parameters it becomes pressing so +admit such types. + +**Proposal** Allow refined types as parent types of classes. All refinements that are inherited in this way become synthetic members of the class. + +**Example** + +```scala +class C: + type T + def m(): T + +type R = C: + type T = Int + def m(): 22 + +class D extends R: + def next(): D +``` +This code now compiles. The definition of `D` is expanded as follows: + +```scala +class D extends C: + def next(): D + /*synthetic*/ type T = Int + /*synthetic*/ def m(): 22 +``` +Note how class refinements are moved from the parent constructor of `D` into the body of class `D` itself. + +This change does not entail a syntax change. Syntactically, parent types cannot be refined types themselves. So the following would be illegal: +```scala +class D extends C { type T = Int; def m(): 22 }: // error + def next(): D +``` +If a refined type should be used directly as a parent type of a class, it needs to come in parentheses: +```scala +class D extends (C { type T = Int; def m(): 22 }) // ok + def next(): D +``` + +## A Small Relaxation To Export Rules + +The rules for export forwarders are changed as follows. + +Previously, all export forwarders were declared `final`. Now, only term members are declared `final`. Type aliases are left aside. + +This makes it possible to export the same type member into several traits and then mix these traits in the same class. `typeclass-aggregates.scala` shows why this is essential to be able to combine multiple givens with type members. + +The change does not lose safety since different type aliases would in any case lead to uninstantiatable classes. \ No newline at end of file diff --git a/docs/sidebar.yml b/docs/sidebar.yml index 65d7ac2f9ee4..7d2ff2532c04 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -153,6 +153,7 @@ subsection: - page: reference/experimental/cc.md - page: reference/experimental/purefuns.md - page: reference/experimental/tupled-function.md + - page: reference/experimental/modularity.md - page: reference/syntax.md - title: Language Versions index: reference/language-versions/language-versions.md diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index 49baa9bc2df6..f1bd43cf7fb7 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -98,6 +98,7 @@ object language: * - ability to merge exported types in intersections * * @see [[https://dotty.epfl.ch/docs/reference/experimental/modularity]] + * @see [[https://dotty.epfl.ch/docs/reference/experimental/typeclasses]] */ @compileTimeOnly("`modularity` can only be used at compile time in import statements") object modularity diff --git a/tasty/src/dotty/tools/tasty/TastyFormat.scala b/tasty/src/dotty/tools/tasty/TastyFormat.scala index e17c98234691..a93198e4e3e4 100644 --- a/tasty/src/dotty/tools/tasty/TastyFormat.scala +++ b/tasty/src/dotty/tools/tasty/TastyFormat.scala @@ -223,6 +223,7 @@ Standard-Section: "ASTs" TopLevelStat* EXPORTED -- An export forwarder OPEN -- an open class INVISIBLE -- invisible during typechecking + TRACKED -- a tracked class parameter / a dependent class Annotation Variance = STABLE -- invariant @@ -504,6 +505,7 @@ object TastyFormat { final val INVISIBLE = 44 final val EMPTYCLAUSE = 45 final val SPLITCLAUSE = 46 + final val TRACKED = 47 // Tree Cat. 2: tag Nat final val firstNatTreeTag = SHAREDterm @@ -693,7 +695,8 @@ object TastyFormat { | INVISIBLE | ANNOTATION | PRIVATEqualified - | PROTECTEDqualified => true + | PROTECTEDqualified + | TRACKED => true case _ => false } diff --git a/tests/neg/i3964.scala b/tests/neg/i3964.scala new file mode 100644 index 000000000000..eaf3953bc230 --- /dev/null +++ b/tests/neg/i3964.scala @@ -0,0 +1,12 @@ +//> using options -source future -language:experimental.modularity +trait Animal +class Dog extends Animal +class Cat extends Animal + +object Test1: + + abstract class Bar { val x: Animal } + val bar: Bar { val x: Cat } = new Bar { val x = new Cat } // error, but should work + + trait Foo { val x: Animal } + val foo: Foo { val x: Cat } = new Foo { val x = new Cat } // error, but should work diff --git a/tests/neg/tracked.check b/tests/neg/tracked.check new file mode 100644 index 000000000000..ae734e7aa0b4 --- /dev/null +++ b/tests/neg/tracked.check @@ -0,0 +1,50 @@ +-- Error: tests/neg/tracked.scala:2:16 --------------------------------------------------------------------------------- +2 |class C(tracked x: Int) // error + | ^ + | `val` or `var` expected +-- [E040] Syntax Error: tests/neg/tracked.scala:7:18 ------------------------------------------------------------------- +7 | def foo(tracked a: Int) = // error + | ^ + | ':' expected, but identifier found +-- Error: tests/neg/tracked.scala:8:12 --------------------------------------------------------------------------------- +8 | tracked val b: Int = 2 // error + | ^^^ + | end of statement expected but 'val' found +-- Error: tests/neg/tracked.scala:11:10 -------------------------------------------------------------------------------- +11 | tracked object Foo // error // error + | ^^^^^^ + | end of statement expected but 'object' found +-- Error: tests/neg/tracked.scala:14:10 -------------------------------------------------------------------------------- +14 | tracked class D // error // error + | ^^^^^ + | end of statement expected but 'class' found +-- Error: tests/neg/tracked.scala:17:10 -------------------------------------------------------------------------------- +17 | tracked type T = Int // error // error + | ^^^^ + | end of statement expected but 'type' found +-- Error: tests/neg/tracked.scala:20:29 -------------------------------------------------------------------------------- +20 | given g2(using tracked val x: Int): C = C(x) // error + | ^^^^^^^^^^^^^^^^^^ + | method parameter x may not be a `val` +-- Error: tests/neg/tracked.scala:4:21 --------------------------------------------------------------------------------- +4 |class C2(tracked var x: Int) // error + | ^ + | mutable variables may not be `tracked` +-- [E006] Not Found Error: tests/neg/tracked.scala:11:2 ---------------------------------------------------------------- +11 | tracked object Foo // error // error + | ^^^^^^^ + | Not found: tracked + | + | longer explanation available when compiling with `-explain` +-- [E006] Not Found Error: tests/neg/tracked.scala:14:2 ---------------------------------------------------------------- +14 | tracked class D // error // error + | ^^^^^^^ + | Not found: tracked + | + | longer explanation available when compiling with `-explain` +-- [E006] Not Found Error: tests/neg/tracked.scala:17:2 ---------------------------------------------------------------- +17 | tracked type T = Int // error // error + | ^^^^^^^ + | Not found: tracked + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/tracked.scala b/tests/neg/tracked.scala new file mode 100644 index 000000000000..8d315a7b89ac --- /dev/null +++ b/tests/neg/tracked.scala @@ -0,0 +1,20 @@ +//> using options -source future -language:experimental.modularity +class C(tracked x: Int) // error + +class C2(tracked var x: Int) // error + +object A: + def foo(tracked a: Int) = // error + tracked val b: Int = 2 // error + +object B: + tracked object Foo // error // error + +object C: + tracked class D // error // error + +object D: + tracked type T = Int // error // error + +object E: + given g2(using tracked val x: Int): C = C(x) // error diff --git a/tests/neg/tracked2.scala b/tests/neg/tracked2.scala new file mode 100644 index 000000000000..2e6fa8cf6045 --- /dev/null +++ b/tests/neg/tracked2.scala @@ -0,0 +1 @@ +class C(tracked val x: Int) // error diff --git a/tests/new/tracked-mixin-traits.scala b/tests/new/tracked-mixin-traits.scala new file mode 100644 index 000000000000..21d890d44f42 --- /dev/null +++ b/tests/new/tracked-mixin-traits.scala @@ -0,0 +1,16 @@ +trait A: + type T +object a extends A: + type T = Int + +trait B(tracked val b: A): + type T = b.T + +trait C(tracked val c: A): + type T = c.T + +class D extends B(a), C(a): + val x: T = 2 + + + diff --git a/tests/pos/depclass-1.scala b/tests/pos/depclass-1.scala new file mode 100644 index 000000000000..38daef85ae98 --- /dev/null +++ b/tests/pos/depclass-1.scala @@ -0,0 +1,19 @@ +//> using options -source future -language:experimental.modularity +class A(tracked val source: String) + +class B(x: Int, tracked val source1: String) extends A(source1) + +class C(tracked val source2: String) extends B(1, source2) + +//class D(source1: String) extends C(source1) +val x = C("hello") +val _: A{ val source: "hello" } = x + +class Vec[Elem](tracked val size: Int) +class Vec8 extends Vec[Float](8) + +val v = Vec[Float](10) +val v2 = Vec8() +val xx: 10 = v.size +val x2: 8 = v2.size + diff --git a/tests/pos/i3920.scala b/tests/pos/i3920.scala new file mode 100644 index 000000000000..6cd74187098f --- /dev/null +++ b/tests/pos/i3920.scala @@ -0,0 +1,32 @@ +//> using options -source future -language:experimental.modularity +trait Ordering { + type T + def compare(t1:T, t2: T): Int +} + +class SetFunctor(tracked val ord: Ordering) { + type Set = List[ord.T] + def empty: Set = Nil + + implicit class helper(s: Set) { + def add(x: ord.T): Set = x :: remove(x) + def remove(x: ord.T): Set = s.filter(e => ord.compare(x, e) != 0) + def member(x: ord.T): Boolean = s.exists(e => ord.compare(x, e) == 0) + } +} + +object Test { + val orderInt = new Ordering { + type T = Int + def compare(t1: T, t2: T): Int = t1 - t2 + } + + val IntSet = new SetFunctor(orderInt) + import IntSet.* + + def main(args: Array[String]) = { + val set = IntSet.empty.add(6).add(8).add(23) + assert(!set.member(7)) + assert(set.member(8)) + } +} \ No newline at end of file diff --git a/tests/pos/i3964.scala b/tests/pos/i3964.scala new file mode 100644 index 000000000000..42412b910899 --- /dev/null +++ b/tests/pos/i3964.scala @@ -0,0 +1,32 @@ +//> using options -source future -language:experimental.modularity +trait Animal +class Dog extends Animal +class Cat extends Animal + +object Test2: + class Bar(tracked val x: Animal) + val b = new Bar(new Cat) + val bar: Bar { val x: Cat } = new Bar(new Cat) // ok + + trait Foo(tracked val x: Animal) + val foo: Foo { val x: Cat } = new Foo(new Cat) {} // ok + +object Test3: + trait Vec(tracked val size: Int) + class Vec8 extends Vec(8) + + abstract class Lst(tracked val size: Int) + class Lst8 extends Lst(8) + + val v8a: Vec { val size: 8 } = new Vec8 + val v8b: Vec { val size: 8 } = new Vec(8) {} + + val l8a: Lst { val size: 8 } = new Lst8 + val l8b: Lst { val size: 8 } = new Lst(8) {} + + class VecN(tracked val n: Int) extends Vec(n) + class Vec9 extends VecN(9) + val v9a = VecN(9) + val _: Vec { val size: 9 } = v9a + val v9b = Vec9() + val _: Vec { val size: 9 } = v9b diff --git a/tests/pos/i3964a/Defs_1.scala b/tests/pos/i3964a/Defs_1.scala new file mode 100644 index 000000000000..7dcc89f7003e --- /dev/null +++ b/tests/pos/i3964a/Defs_1.scala @@ -0,0 +1,18 @@ +//> using options -source future -language:experimental.modularity +trait Animal +class Dog extends Animal +class Cat extends Animal + +object Test2: + class Bar(tracked val x: Animal) + val b = new Bar(new Cat) + val bar: Bar { val x: Cat } = new Bar(new Cat) // ok + + trait Foo(tracked val x: Animal) + val foo: Foo { val x: Cat } = new Foo(new Cat) {} // ok + +package coll: + trait Vec(tracked val size: Int) + class Vec8 extends Vec(8) + + abstract class Lst(tracked val size: Int) \ No newline at end of file diff --git a/tests/pos/i3964a/Uses_2.scala b/tests/pos/i3964a/Uses_2.scala new file mode 100644 index 000000000000..9d1b6ebaa58b --- /dev/null +++ b/tests/pos/i3964a/Uses_2.scala @@ -0,0 +1,16 @@ +//> using options -source future -language:experimental.modularity +import coll.* +class Lst8 extends Lst(8) + +val v8a: Vec { val size: 8 } = new Vec8 +val v8b: Vec { val size: 8 } = new Vec(8) {} + +val l8a: Lst { val size: 8 } = new Lst8 +val l8b: Lst { val size: 8 } = new Lst(8) {} + +class VecN(tracked val n: Int) extends Vec(n) +class Vec9 extends VecN(9) +val v9a = VecN(9) +val _: Vec { val size: 9 } = v9a +val v9b = Vec9() +val _: Vec { val size: 9 } = v9b diff --git a/tests/pos/parsercombinators-expanded.scala b/tests/pos/parsercombinators-expanded.scala new file mode 100644 index 000000000000..cf8137bfe8eb --- /dev/null +++ b/tests/pos/parsercombinators-expanded.scala @@ -0,0 +1,64 @@ +//> using options -source future -language:experimental.modularity + +import collection.mutable + +/// A parser combinator. +trait Combinator[T]: + + /// The context from which elements are being parsed, typically a stream of tokens. + type Context + /// The element being parsed. + type Element + + extension (self: T) + /// Parses and returns an element from `context`. + def parse(context: Context): Option[Element] +end Combinator + +final case class Apply[C, E](action: C => Option[E]) +final case class Combine[A, B](first: A, second: B) + +object test: + + class apply[C, E] extends Combinator[Apply[C, E]]: + type Context = C + type Element = E + extension(self: Apply[C, E]) + def parse(context: C): Option[E] = self.action(context) + + def apply[C, E]: apply[C, E] = new apply[C, E] + + class combine[A, B]( + tracked val f: Combinator[A], + tracked val s: Combinator[B] { type Context = f.Context} + ) extends Combinator[Combine[A, B]]: + type Context = f.Context + type Element = (f.Element, s.Element) + extension(self: Combine[A, B]) + def parse(context: Context): Option[Element] = ??? + + def combine[A, B]( + _f: Combinator[A], + _s: Combinator[B] { type Context = _f.Context} + ) = new combine[A, B](_f, _s) + // cast is needed since the type of new combine[A, B](_f, _s) + // drops the required refinement. + + extension [A] (buf: mutable.ListBuffer[A]) def popFirst() = + if buf.isEmpty then None + else try Some(buf.head) finally buf.remove(0) + + @main def hello: Unit = { + val source = (0 to 10).toList + val stream = source.to(mutable.ListBuffer) + + val n = Apply[mutable.ListBuffer[Int], Int](s => s.popFirst()) + val m = Combine(n, n) + + val c = combine( + apply[mutable.ListBuffer[Int], Int], + apply[mutable.ListBuffer[Int], Int] + ) + val r = c.parse(m)(stream) // was type mismatch, now OK + val rc: Option[(Int, Int)] = r + } diff --git a/tests/pos/parsercombinators-givens-2.scala b/tests/pos/parsercombinators-givens-2.scala new file mode 100644 index 000000000000..8349d69a30af --- /dev/null +++ b/tests/pos/parsercombinators-givens-2.scala @@ -0,0 +1,52 @@ +//> using options -source future -language:experimental.modularity + +import collection.mutable + +/// A parser combinator. +trait Combinator[T]: + + /// The context from which elements are being parsed, typically a stream of tokens. + type Context + /// The element being parsed. + type Element + + extension (self: T) + /// Parses and returns an element from `context`. + def parse(context: Context): Option[Element] +end Combinator + +final case class Apply[C, E](action: C => Option[E]) +final case class Combine[A, B](first: A, second: B) + +given apply[C, E]: Combinator[Apply[C, E]] with { + type Context = C + type Element = E + extension(self: Apply[C, E]) { + def parse(context: C): Option[E] = self.action(context) + } +} + +given combine[A, B, C](using + f: Combinator[A] { type Context = C }, + s: Combinator[B] { type Context = C } +): Combinator[Combine[A, B]] with { + type Context = f.Context + type Element = (f.Element, s.Element) + extension(self: Combine[A, B]) { + def parse(context: Context): Option[Element] = ??? + } +} + +extension [A] (buf: mutable.ListBuffer[A]) def popFirst() = + if buf.isEmpty then None + else try Some(buf.head) finally buf.remove(0) + +@main def hello: Unit = { + val source = (0 to 10).toList + val stream = source.to(mutable.ListBuffer) + + val n = Apply[mutable.ListBuffer[Int], Int](s => s.popFirst()) + val m = Combine(n, n) + + val r = m.parse(stream) // works, but Element type is not resolved correctly +} diff --git a/tests/pos/parsercombinators-givens.scala b/tests/pos/parsercombinators-givens.scala new file mode 100644 index 000000000000..5b5588c93840 --- /dev/null +++ b/tests/pos/parsercombinators-givens.scala @@ -0,0 +1,54 @@ +//> using options -source future -language:experimental.modularity + +import collection.mutable + +/// A parser combinator. +trait Combinator[T]: + + /// The context from which elements are being parsed, typically a stream of tokens. + type Context + /// The element being parsed. + type Element + + extension (self: T) + /// Parses and returns an element from `context`. + def parse(context: Context): Option[Element] +end Combinator + +final case class Apply[C, E](action: C => Option[E]) +final case class Combine[A, B](first: A, second: B) + +given apply[C, E]: Combinator[Apply[C, E]] with { + type Context = C + type Element = E + extension(self: Apply[C, E]) { + def parse(context: C): Option[E] = self.action(context) + } +} + +given combine[A, B](using + tracked val f: Combinator[A], + tracked val s: Combinator[B] { type Context = f.Context } +): Combinator[Combine[A, B]] with { + type Context = f.Context + type Element = (f.Element, s.Element) + extension(self: Combine[A, B]) { + def parse(context: Context): Option[Element] = ??? + } +} + +extension [A] (buf: mutable.ListBuffer[A]) def popFirst() = + if buf.isEmpty then None + else try Some(buf.head) finally buf.remove(0) + +@main def hello: Unit = { + val source = (0 to 10).toList + val stream = source.to(mutable.ListBuffer) + + val n = Apply[mutable.ListBuffer[Int], Int](s => s.popFirst()) + val m = Combine(n, n) + + val r = m.parse(stream) // error: type mismatch, found `mutable.ListBuffer[Int]`, required `?1.Context` + val rc: Option[(Int, Int)] = r + // it would be great if this worked +} diff --git a/tests/run/i3920.scala b/tests/run/i3920.scala new file mode 100644 index 000000000000..c66fd8908976 --- /dev/null +++ b/tests/run/i3920.scala @@ -0,0 +1,26 @@ +//> using options -source future -language:experimental.modularity +trait Ordering: + type T + def compare(t1:T, t2: T): Int + +class SetFunctor(tracked val ord: Ordering): + type Set = List[ord.T] + + def empty: Set = Nil + + extension (s: Set) + def add(x: ord.T): Set = x :: remove(x) + def remove(x: ord.T): Set = s.filter(e => ord.compare(x, e) != 0) + def contains(x: ord.T): Boolean = s.exists(e => ord.compare(x, e) == 0) + +object intOrdering extends Ordering: + type T = Int + def compare(t1: T, t2: T): Int = t1 - t2 + +val IntSet = new SetFunctor(intOrdering) + +@main def Test = + import IntSet.* + val set = IntSet.empty.add(6).add(8).add(23) + assert(!set.contains(7)) + assert(set.contains(8)) \ No newline at end of file From 7a46264a8e8c1553a0b1e364c46d61bb9cee8407 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Jan 2024 19:12:34 +0100 Subject: [PATCH 06/63] Some tweaks to modularity.md --- .../reference/experimental/modularity.md | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/docs/_docs/reference/experimental/modularity.md b/docs/_docs/reference/experimental/modularity.md index ccbd141bad4e..1936bcb34ee6 100644 --- a/docs/_docs/reference/experimental/modularity.md +++ b/docs/_docs/reference/experimental/modularity.md @@ -9,7 +9,7 @@ nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/modularity. Martin Odersky, 7.1.2024 Scala is a language in the SML tradition, in the sense that it has -abstract and alias types as members of classes. This leads to a simple dependently +abstract and alias types as members of modules (which in Scala take the form of objects and classes). This leads to a simple dependently typed system, where dependencies in types are on paths instead of full terms. So far, some key ingredients were lacking which meant that module composition with functors is harder in Scala than in SML. In particular, one often needs to resort the infamous `Aux` pattern that lifts type members into type parameters so that they can be tracked across class instantiations. This makes modular, dependently typed programs @@ -19,10 +19,10 @@ In this note I propose some small changes to Scala's dependent typing that makes modular programming much more straightforward. The suggested improvements have been implemented and are available -in source version `future` if the additional experimental language import `modularity` is present. For instance, using +in source version `future` if the additional experimental language import `modularity` is present. For instance, using the following command: ``` - scala compile -source future -language experimental.modularity + scala compile -source:future -language:experimental.modularity ``` ## Tracked Parameters @@ -41,7 +41,7 @@ For instance, consider the following definitions: ``` Then `f(y)` would have type `Int`, since the compiler will substitute the concrete parameter reference `y` for the formal parameter `x` in the result -type of `f`. +type of `f`, and `y.T = Int` However, if we use a class `F` instead of a method `f`, things go wrong. @@ -66,7 +66,7 @@ Then the constructor `F` would get roughly the following type: ```scala F(x1: C): F { val x: x1.type } ``` -_Aside:_ More precisely, both parameter and refinement would apply to the same name `x` but the refinement still refers to the parameter. We unfortunately can't express that in source, however, so we chose the new name `x1` for the parameter. +_Aside:_ More precisely, both parameter and refinement would apply to the same name `x` but the refinement still refers to the parameter. We unfortunately can't express that in source, however, so we chose the new name `x1` for the parameter in the explanation. With the new constructor type, the expression `F(y).result` would now have the type `Int`, as hoped for. The reasoning to get there is as follows: @@ -118,23 +118,21 @@ The (soft) `tracked` modifier is only allowed for `val` parameters of classes. **Discussion** -Since `tracked` is so useful, why not assume it by default? First, `tracked` makes sense only for `val` parameters. If a class parameter is not also a field declared using `val` then there's nothing to refine in the constructor. -But making all `val` parameters tracked by default would also be a backwards -incompatible change. For instance, the following code would break: +Since `tracked` is so useful, why not assume it by default? First, `tracked` makes sense only for `val` parameters. If a class parameter is not also a field declared using `val` then there's nothing to refine in the constructor result type. One could think of at least making all `val` parameters tracked by default, but that would be a backwards incompatible change. For instance, the following code would break: ```scala case class Foo(x: Int) var foo = Foo(1) if someCondition then foo = Foo(2) ``` -if we assume `tracked` for parameter `x` (which is implicitly a `val`), +If we assume `tracked` for parameter `x` (which is implicitly a `val`), then `foo` would get inferred type `Foo { val x: 1 }`, so it could not -be reassigned to a value of type `Foo { val x: 2 }`. +be reassigned to a value of type `Foo { val x: 2 }` on the next line. Another approach might be to assume `tracked` for a `val` parameter `x` only if the class refers to a type member of `x`. But it turns out that this scheme is unimplementable since it would quickly lead to cyclic references -in recursive class graphs. So an explicit `tracked` looks like the best feasible option. +when typechecking recursive class graphs. So an explicit `tracked` looks like the best available option. ## Allow Class Parents to be Refined Types @@ -186,6 +184,6 @@ The rules for export forwarders are changed as follows. Previously, all export forwarders were declared `final`. Now, only term members are declared `final`. Type aliases are left aside. -This makes it possible to export the same type member into several traits and then mix these traits in the same class. `typeclass-aggregates.scala` shows why this is essential to be able to combine multiple givens with type members. +This makes it possible to export the same type member into several traits and then mix these traits in the same class. The test file `tests/pos/typeclass-aggregates.scala` shows why this is essential if we want to combine multiple givens with type members in a new given that aggregates all these givens in an intersection type. The change does not lose safety since different type aliases would in any case lead to uninstantiatable classes. \ No newline at end of file From 19970450cc243a39275a00d0b594036a4b4013ca Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 25 Dec 2023 11:37:57 +0100 Subject: [PATCH 07/63] Make some context bound evidence params tracked Make context bound evidence params tracked if they have types with abstract type members. --- .../src/dotty/tools/dotc/core/Symbols.scala | 3 ++- .../src/dotty/tools/dotc/typer/Namer.scala | 27 ++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Symbols.scala b/compiler/src/dotty/tools/dotc/core/Symbols.scala index 78c736649605..096e9c30dd70 100644 --- a/compiler/src/dotty/tools/dotc/core/Symbols.scala +++ b/compiler/src/dotty/tools/dotc/core/Symbols.scala @@ -308,7 +308,6 @@ object Symbols extends SymUtils { * With the given setup, all such calls will give implicit-not found errors */ final def symbol(implicit ev: DontUseSymbolOnSymbol): Nothing = unsupported("symbol") - type DontUseSymbolOnSymbol final def source(using Context): SourceFile = { def valid(src: SourceFile): SourceFile = @@ -932,6 +931,8 @@ object Symbols extends SymUtils { case (x: Symbol) :: _ if x.isType => Some(xs.asInstanceOf[List[TypeSymbol]]) case _ => None + type DontUseSymbolOnSymbol + // ----- Locating predefined symbols ---------------------------------------- def requiredPackage(path: PreName)(using Context): TermSymbol = { diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 34960cafcf3b..4eeffbbec535 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -7,7 +7,7 @@ import ast.* import Trees.*, StdNames.*, Scopes.*, Denotations.*, NamerOps.*, ContextOps.* import Contexts.*, Symbols.*, Types.*, SymDenotations.*, Names.*, NameOps.*, Flags.* import Decorators.*, Comments.{_, given} -import NameKinds.DefaultGetterName +import NameKinds.{DefaultGetterName, ContextBoundParamName} import ast.desugar, ast.desugar.* import ProtoTypes.* import util.Spans.* @@ -1813,7 +1813,7 @@ class Namer { typer: Typer => } /** The type signature of a DefDef with given symbol */ - def defDefSig(ddef: DefDef, sym: Symbol, completer: Namer#Completer)(using Context): Type = { + def defDefSig(ddef: DefDef, sym: Symbol, completer: Namer#Completer)(using Context): Type = // Beware: ddef.name need not match sym.name if sym was freshened! val isConstructor = sym.name == nme.CONSTRUCTOR @@ -1849,16 +1849,37 @@ class Namer { typer: Typer => ddef.trailingParamss.foreach(completeParams) val paramSymss = normalizeIfConstructor(ddef.paramss.nestedMap(symbolOfTree), isConstructor) sym.setParamss(paramSymss) + + /** Set all class parameters with types that have an abstract type member + * to be tracked, provided they are `val` parameters, or context bound + * evidence parameters. In the second case, reset any private and local + * flags for context bound evidence parameter so that it becomes a `val`. + */ + def setTracked(sym: Symbol): Unit = sym.maybeOwner.maybeOwner.infoOrCompleter match + case info: TempClassInfo + if !sym.is(Tracked) + && sym.name.is(ContextBoundParamName) + && sym.info.memberNames(abstractTypeNameFilter).nonEmpty => + typr.println(i"set tracked $sym: ${sym.info} containing ${sym.info.memberNames(abstractTypeNameFilter).toList}") + for acc <- info.decls.lookupAll(sym.name) if acc.is(ParamAccessor) do + acc.resetFlag(PrivateLocal) + acc.setFlag(Tracked) + sym.setFlag(Tracked) + case _ => + def wrapMethType(restpe: Type): Type = instantiateDependent(restpe, paramSymss) methodType(paramSymss, restpe, ddef.mods.is(JavaDefined)) + if isConstructor then + if sym.isPrimaryConstructor && Feature.enabled(modularity) then + paramSymss.foreach(_.foreach(setTracked)) // set result type tree to unit, but take the current class as result type of the symbol typedAheadType(ddef.tpt, defn.UnitType) wrapMethType(effectiveResultType(sym, paramSymss)) else valOrDefDefSig(ddef, sym, paramSymss, wrapMethType) - } + end defDefSig def inferredResultType( mdef: ValOrDefDef, From 10e25b6c231c9091c15dc6eb8b6f26cd09dfb884 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 23 Dec 2023 16:14:56 +0100 Subject: [PATCH 08/63] Make explicit arguments for context bounds an error from 3.5 --- compiler/src/dotty/tools/dotc/typer/ReTyper.scala | 1 + compiler/src/dotty/tools/dotc/typer/Typer.scala | 3 +++ tests/neg/context-bounds-migration.scala | 10 ++++++++++ 3 files changed, 14 insertions(+) create mode 100644 tests/neg/context-bounds-migration.scala diff --git a/compiler/src/dotty/tools/dotc/typer/ReTyper.scala b/compiler/src/dotty/tools/dotc/typer/ReTyper.scala index e152b5e6b9c7..253c4fda9396 100644 --- a/compiler/src/dotty/tools/dotc/typer/ReTyper.scala +++ b/compiler/src/dotty/tools/dotc/typer/ReTyper.scala @@ -189,4 +189,5 @@ class ReTyper(nestingLevel: Int = 0) extends Typer(nestingLevel) with ReChecking override protected def checkEqualityEvidence(tree: tpd.Tree, pt: Type)(using Context): Unit = () override protected def matchingApply(methType: MethodOrPoly, pt: FunProto)(using Context): Boolean = true override protected def typedScala2MacroBody(call: untpd.Tree)(using Context): Tree = promote(call) + override protected def migrate[T](migration: => T, disabled: => T = ()): T = disabled } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index b601def0398e..55282c597781 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -157,6 +157,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // Overridden in derived typers def newLikeThis(nestingLevel: Int): Typer = new Typer(nestingLevel) + // Overridden to do nothing in derived typers + protected def migrate[T](migration: => T, disabled: => T = ()): T = migration + /** Find the type of an identifier with given `name` in given context `ctx`. * @param name the name of the identifier * @param pt the expected type diff --git a/tests/neg/context-bounds-migration.scala b/tests/neg/context-bounds-migration.scala new file mode 100644 index 000000000000..b27dc884692c --- /dev/null +++ b/tests/neg/context-bounds-migration.scala @@ -0,0 +1,10 @@ +//> using options -Xfatal-warnings + +class C[T] +def foo[X: C] = () + +given [T]: C[T] = C[T]() + +def Test = + foo(C[Int]()) // error + foo(using C[Int]()) // ok From 8cafe028ffeb4bb63064bd4d79b8e14433b4a4e5 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Jan 2024 19:58:15 +0100 Subject: [PATCH 09/63] Use the name of the type parameter as context bound evidence - Don't rely on ContextBoundParamName for desugaring - For the first context bound [X: B ...], use `X` as the name of the evidence parameter for `B`. --- .../src/dotty/tools/dotc/ast/Desugar.scala | 105 ++++++++++-------- .../src/dotty/tools/dotc/typer/Namer.scala | 38 ++++--- .../dotty/tools/dotc/util/Signatures.scala | 4 +- .../test/dotc/pos-test-pickling.blacklist | 3 +- tests/pos/parsercombinators-ctx-bounds.scala | 49 ++++++++ 5 files changed, 132 insertions(+), 67 deletions(-) create mode 100644 tests/pos/parsercombinators-ctx-bounds.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index e4bddcd33ede..2ff6e828e769 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -10,7 +10,7 @@ import Annotations.Annotation import NameKinds.{UniqueName, ContextBoundParamName, ContextFunctionParamName, DefaultGetterName, WildcardParamName} import typer.{Namer, Checking} import util.{Property, SourceFile, SourcePosition, Chars} -import config.Feature.{sourceVersion, migrateTo3, enabled} +import config.Feature import config.SourceVersion.* import collection.mutable.ListBuffer import reporting.* @@ -46,6 +46,11 @@ object desugar { */ val UntupledParam: Property.Key[Unit] = Property.StickyKey() + /** An attachment key to indicate that a ValDef is an evidence parameter + * for a context bound. + */ + val ContextBoundParam: Property.Key[Unit] = Property.StickyKey() + /** What static check should be applied to a Match? */ enum MatchCheck { case None, Exhaustive, IrrefutablePatDef, IrrefutableGenFrom @@ -195,17 +200,6 @@ object desugar { else vdef1 end valDef - def makeImplicitParameters( - tpts: List[Tree], implicitFlag: FlagSet, - mkParamName: Int => TermName, - forPrimaryConstructor: Boolean = false - )(using Context): List[ValDef] = - for (tpt, i) <- tpts.zipWithIndex yield { - val paramFlags: FlagSet = if (forPrimaryConstructor) LocalParamAccessor else Param - val epname = mkParamName(i) - ValDef(epname, tpt, EmptyTree).withFlags(paramFlags | implicitFlag) - } - def mapParamss(paramss: List[ParamClause]) (mapTypeParam: TypeDef => TypeDef) (mapTermParam: ValDef => ValDef)(using Context): List[ParamClause] = @@ -235,31 +229,38 @@ object desugar { private def elimContextBounds(meth: DefDef, isPrimaryConstructor: Boolean)(using Context): DefDef = val DefDef(_, paramss, tpt, rhs) = meth val evidenceParamBuf = ListBuffer[ValDef]() - var seenContextBounds: Int = 0 - def desugarContextBounds(rhs: Tree): Tree = rhs match + + def desugarContextBounds(tparam: TypeDef, rhs: Tree): Tree = rhs match case ContextBounds(tbounds, cxbounds) => - val iflag = if sourceVersion.isAtLeast(`future`) then Given else Implicit - evidenceParamBuf ++= makeImplicitParameters( - cxbounds, iflag, - // Just like with `makeSyntheticParameter` on nameless parameters of - // using clauses, we only need names that are unique among the - // parameters of the method since shadowing does not affect - // implicit resolution in Scala 3. - mkParamName = i => - val index = seenContextBounds + 1 // Start at 1 like FreshNameCreator. - val ret = ContextBoundParamName(EmptyTermName, index) - seenContextBounds += 1 - ret, - forPrimaryConstructor = isPrimaryConstructor) + val iflag = if Feature.sourceVersion.isAtLeast(`future`) then Given else Implicit + val flags = if isPrimaryConstructor then iflag | LocalParamAccessor else iflag | Param + var useParamName = Feature.enabled(Feature.modularity) + for bound <- cxbounds do + val paramName = + if useParamName then + useParamName = false + tparam.name.toTermName + else + seenContextBounds += 1 // Start at 1 like FreshNameCreator. + ContextBoundParamName(EmptyTermName, seenContextBounds) + // Just like with `makeSyntheticParameter` on nameless parameters of + // using clauses, we only need names that are unique among the + // parameters of the method since shadowing does not affect + // implicit resolution in Scala 3. + val evidenceParam = ValDef(paramName, bound, EmptyTree).withFlags(flags) + evidenceParam.pushAttachment(ContextBoundParam, ()) + evidenceParamBuf += evidenceParam tbounds case LambdaTypeTree(tparams, body) => - cpy.LambdaTypeTree(rhs)(tparams, desugarContextBounds(body)) + cpy.LambdaTypeTree(rhs)(tparams, desugarContextBounds(tparam, body)) case _ => rhs + end desugarContextBounds + val paramssNoContextBounds = mapParamss(paramss) { - tparam => cpy.TypeDef(tparam)(rhs = desugarContextBounds(tparam.rhs)) + tparam => cpy.TypeDef(tparam)(rhs = desugarContextBounds(tparam, tparam.rhs)) }(identity) rhs match @@ -419,7 +420,7 @@ object desugar { private def evidenceParams(meth: DefDef)(using Context): List[ValDef] = meth.paramss.reverse match { case ValDefs(vparams @ (vparam :: _)) :: _ if vparam.mods.isOneOf(GivenOrImplicit) => - vparams.takeWhile(_.name.is(ContextBoundParamName)) + vparams.takeWhile(_.hasAttachment(ContextBoundParam)) case _ => Nil } @@ -431,11 +432,18 @@ object desugar { if (!keepAnnotations) mods = mods.withAnnotations(Nil) tparam.withMods(mods & EmptyFlags | Param) } - private def toDefParam(vparam: ValDef, keepAnnotations: Boolean, keepDefault: Boolean): ValDef = { + + private def toDefParam(vparam: ValDef, keepAnnotations: Boolean, keepDefault: Boolean)(using Context): ValDef = { var mods = vparam.rawMods if (!keepAnnotations) mods = mods.withAnnotations(Nil) val hasDefault = if keepDefault then HasDefault else EmptyFlags - vparam.withMods(mods & (GivenOrImplicit | Erased | hasDefault | Tracked) | Param) + // Need to ensure that tree is duplicated since term parameters can be watched + // and cloning a term parameter will copy its watchers to the clone, which means + // we'd get cross-talk between the original parameter and the clone. + ValDef(vparam.name, vparam.tpt, vparam.rhs) + .withSpan(vparam.span) + .withAttachmentsFrom(vparam) + .withMods(mods & (GivenOrImplicit | Erased | hasDefault | Tracked) | Param) } def mkApply(fn: Tree, paramss: List[ParamClause])(using Context): Tree = @@ -679,7 +687,7 @@ object desugar { } ensureApplied(nu) - val copiedAccessFlags = if migrateTo3 then EmptyFlags else AccessFlags + val copiedAccessFlags = if Feature.migrateTo3 then EmptyFlags else AccessFlags // Methods to add to a case class C[..](p1: T1, ..., pN: Tn)(moreParams) // def _1: T1 = this.p1 @@ -862,11 +870,17 @@ object desugar { Nil } else { - val defParamss = defVparamss match - case Nil :: paramss => - paramss // drop leading () that got inserted by class - // TODO: drop this once we do not silently insert empty class parameters anymore - case paramss => paramss + val defParamss = defVparamss.nestedMapConserve: param => + // for named context bound parameters, we assume that they might have embedded types + // so they should be treated as tracked. + if param.hasAttachment(ContextBoundParam) && !param.name.is(ContextBoundParamName) + then param.withFlags(param.mods.flags | Tracked) + else param + match + case Nil :: paramss => + paramss // drop leading () that got inserted by class + // TODO: drop this once we do not silently insert empty class parameters anymore + case paramss => paramss val finalFlag = if ctx.settings.YcompileScala2Library.value then EmptyFlags else Final // implicit wrapper is typechecked in same scope as constructor, so // we can reuse the constructor parameters; no derived params are needed. @@ -1613,14 +1627,13 @@ object desugar { .collect: case vd: ValDef => vd - def makeContextualFunction(formals: List[Tree], paramNamesOrNil: List[TermName], body: Tree, erasedParams: List[Boolean])(using Context): Function = { - val mods = Given - val params = makeImplicitParameters(formals, mods, - mkParamName = i => - if paramNamesOrNil.isEmpty then ContextFunctionParamName.fresh() - else paramNamesOrNil(i)) - FunctionWithMods(params, body, Modifiers(mods), erasedParams) - } + def makeContextualFunction(formals: List[Tree], paramNamesOrNil: List[TermName], body: Tree, erasedParams: List[Boolean])(using Context): Function = + val paramNames = + if paramNamesOrNil.nonEmpty then paramNamesOrNil + else formals.map(_ => ContextFunctionParamName.fresh()) + val params = for (tpt, pname) <- formals.zip(paramNames) yield + ValDef(pname, tpt, EmptyTree).withFlags(Given | Param) + FunctionWithMods(params, body, Modifiers(Given), erasedParams) private def derivedValDef(original: Tree, named: NameTree, tpt: Tree, rhs: Tree, mods: Modifiers)(using Context) = { val vdef = ValDef(named.name.asTermName, tpt, rhs) diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 4eeffbbec535..13b24d704b05 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -122,7 +122,8 @@ class Namer { typer: Typer => /** Record `sym` as the symbol defined by `tree` */ def recordSym(sym: Symbol, tree: Tree)(using Context): Symbol = { - for (refs <- tree.removeAttachment(References); ref <- refs) ref.watching(sym) + for refs <- tree.removeAttachment(References); ref <- refs do + ref.watching(sym) tree.pushAttachment(SymOfTree, sym) sym } @@ -525,11 +526,9 @@ class Namer { typer: Typer => } /** Transfer all references to `from` to `to` */ - def transferReferences(from: ValDef, to: ValDef): Unit = { - val fromRefs = from.removeAttachment(References).getOrElse(Nil) - val toRefs = to.removeAttachment(References).getOrElse(Nil) - to.putAttachment(References, fromRefs ++ toRefs) - } + def transferReferences(from: ValDef, to: ValDef): Unit = + for ref <- from.removeAttachment(References).getOrElse(Nil) do + ref.watching(to) /** Merge the module class `modCls` in the expanded tree of `mdef` with the * body and derived clause of the synthetic module class `fromCls`. @@ -1855,17 +1854,20 @@ class Namer { typer: Typer => * evidence parameters. In the second case, reset any private and local * flags for context bound evidence parameter so that it becomes a `val`. */ - def setTracked(sym: Symbol): Unit = sym.maybeOwner.maybeOwner.infoOrCompleter match - case info: TempClassInfo - if !sym.is(Tracked) - && sym.name.is(ContextBoundParamName) - && sym.info.memberNames(abstractTypeNameFilter).nonEmpty => - typr.println(i"set tracked $sym: ${sym.info} containing ${sym.info.memberNames(abstractTypeNameFilter).toList}") - for acc <- info.decls.lookupAll(sym.name) if acc.is(ParamAccessor) do - acc.resetFlag(PrivateLocal) - acc.setFlag(Tracked) - sym.setFlag(Tracked) - case _ => + def setTracked(param: ValDef): Unit = + val sym = symbolOfTree(param) + sym.maybeOwner.maybeOwner.infoOrCompleter match + case info: TempClassInfo => + if !sym.is(Tracked) + && param.hasAttachment(ContextBoundParam) + && sym.info.memberNames(abstractTypeNameFilter).nonEmpty + then + typr.println(i"set tracked $param, $sym: ${sym.info} containing ${sym.info.memberNames(abstractTypeNameFilter).toList}") + for acc <- info.decls.lookupAll(sym.name) if acc.is(ParamAccessor) do + acc.resetFlag(PrivateLocal) + acc.setFlag(Tracked) + sym.setFlag(Tracked) + case _ => def wrapMethType(restpe: Type): Type = instantiateDependent(restpe, paramSymss) @@ -1873,7 +1875,7 @@ class Namer { typer: Typer => if isConstructor then if sym.isPrimaryConstructor && Feature.enabled(modularity) then - paramSymss.foreach(_.foreach(setTracked)) + ddef.termParamss.foreach(_.foreach(setTracked)) // set result type tree to unit, but take the current class as result type of the symbol typedAheadType(ddef.tpt, defn.UnitType) wrapMethType(effectiveResultType(sym, paramSymss)) diff --git a/compiler/src/dotty/tools/dotc/util/Signatures.scala b/compiler/src/dotty/tools/dotc/util/Signatures.scala index 0bd407261125..736633e0f6a7 100644 --- a/compiler/src/dotty/tools/dotc/util/Signatures.scala +++ b/compiler/src/dotty/tools/dotc/util/Signatures.scala @@ -495,8 +495,8 @@ object Signatures { case res => List(tpe) def isSyntheticEvidence(name: String) = - if !name.startsWith(NameKinds.ContextBoundParamName.separator) then false else - symbol.paramSymss.flatten.find(_.name.show == name).exists(_.flags.is(Flags.Implicit)) + name.startsWith(NameKinds.ContextBoundParamName.separator) + && symbol.paramSymss.flatten.find(_.name.show == name).exists(_.flags.is(Flags.Implicit)) def toTypeParam(tpe: PolyType): List[Param] = val evidenceParams = (tpe.paramNamess.flatten zip tpe.paramInfoss.flatten).flatMap: diff --git a/compiler/test/dotc/pos-test-pickling.blacklist b/compiler/test/dotc/pos-test-pickling.blacklist index 2b28cc078a8c..53aa1c31aeea 100644 --- a/compiler/test/dotc/pos-test-pickling.blacklist +++ b/compiler/test/dotc/pos-test-pickling.blacklist @@ -119,4 +119,5 @@ i7445b.scala i15525.scala # alias types at different levels of dereferencing -parsercombinators-givens.scala \ No newline at end of file +parsercombinators-givens.scala +parsercombinators-ctx-bounds.scala diff --git a/tests/pos/parsercombinators-ctx-bounds.scala b/tests/pos/parsercombinators-ctx-bounds.scala new file mode 100644 index 000000000000..d77abea5e539 --- /dev/null +++ b/tests/pos/parsercombinators-ctx-bounds.scala @@ -0,0 +1,49 @@ +//> using options -language:experimental.modularity -source future +import collection.mutable + +/// A parser combinator. +trait Combinator[T]: + + /// The context from which elements are being parsed, typically a stream of tokens. + type Context + /// The element being parsed. + type Element + + extension (self: T) + /// Parses and returns an element from `context`. + def parse(context: Context): Option[Element] +end Combinator + +final case class Apply[C, E](action: C => Option[E]) +final case class Combine[A, B](first: A, second: B) + +given apply[C, E]: Combinator[Apply[C, E]] with { + type Context = C + type Element = E + extension(self: Apply[C, E]) { + def parse(context: C): Option[E] = self.action(context) + } +} + +given combine[A: Combinator, B: [X] =>> Combinator[X] { type Context = A.Context }] + : Combinator[Combine[A, B]] with + type Context = A.Context + type Element = (A.Element, B.Element) + extension(self: Combine[A, B]) + def parse(context: Context): Option[Element] = ??? + +extension [A] (buf: mutable.ListBuffer[A]) def popFirst() = + if buf.isEmpty then None + else try Some(buf.head) finally buf.remove(0) + +@main def hello: Unit = { + val source = (0 to 10).toList + val stream = source.to(mutable.ListBuffer) + + val n = Apply[mutable.ListBuffer[Int], Int](s => s.popFirst()) + val m = Combine(n, n) + + val r = m.parse(stream) // error: type mismatch, found `mutable.ListBuffer[Int]`, required `?1.Context` + val rc: Option[(Int, Int)] = r + // it would be great if this worked +} From d712f151fe6d3fc10f6f9a67a38d183c7d219446 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Jan 2024 21:15:19 +0100 Subject: [PATCH 10/63] Allow contecxt bounds with abstract `This` types If a context bound type `T` for type parameter `A` does not have type parameters, demand evidence of type `T { type This = A }` instead. --- .../src/dotty/tools/dotc/ast/Desugar.scala | 2 +- compiler/src/dotty/tools/dotc/ast/untpd.scala | 8 + .../dotty/tools/dotc/parsing/Parsers.scala | 2 +- .../tools/dotc/printing/RefinedPrinter.scala | 4 +- .../src/dotty/tools/dotc/typer/Namer.scala | 13 +- .../src/dotty/tools/dotc/typer/Typer.scala | 14 ++ .../test/dotc/pos-test-pickling.blacklist | 2 + tests/neg/i9330.scala | 2 +- tests/pos/parsercombinators-this.scala | 53 +++++++ tests/pos/typeclasses-this.scala | 144 ++++++++++++++++++ 10 files changed, 235 insertions(+), 9 deletions(-) create mode 100644 tests/pos/parsercombinators-this.scala create mode 100644 tests/pos/typeclasses-this.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 2ff6e828e769..16b11a3c6f68 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -241,7 +241,7 @@ object desugar { if useParamName then useParamName = false tparam.name.toTermName - else + else seenContextBounds += 1 // Start at 1 like FreshNameCreator. ContextBoundParamName(EmptyTermName, seenContextBounds) // Just like with `makeSyntheticParameter` on nameless parameters of diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index def0ca4d755c..223d9d01e942 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -118,6 +118,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case class ContextBounds(bounds: TypeBoundsTree, cxBounds: List[Tree])(implicit @constructorOnly src: SourceFile) extends TypTree case class PatDef(mods: Modifiers, pats: List[Tree], tpt: Tree, rhs: Tree)(implicit @constructorOnly src: SourceFile) extends DefTree case class ExtMethods(paramss: List[ParamClause], methods: List[Tree])(implicit @constructorOnly src: SourceFile) extends Tree + case class ContextBoundTypeTree(tycon: Tree, paramName: TypeName)(implicit @constructorOnly src: SourceFile) extends Tree case class MacroTree(expr: Tree)(implicit @constructorOnly src: SourceFile) extends Tree case class ImportSelector(imported: Ident, renamed: Tree = EmptyTree, bound: Tree = EmptyTree)(implicit @constructorOnly src: SourceFile) extends Tree { @@ -677,6 +678,9 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { def ExtMethods(tree: Tree)(paramss: List[ParamClause], methods: List[Tree])(using Context): Tree = tree match case tree: ExtMethods if (paramss eq tree.paramss) && (methods == tree.methods) => tree case _ => finalize(tree, untpd.ExtMethods(paramss, methods)(tree.source)) + def ContextBoundTypeTree(tree: Tree)(tycon: Tree, paramName: TypeName)(using Context): Tree = tree match + case tree: ContextBoundTypeTree if (tycon eq tree.tycon) && paramName == tree.paramName => tree + case _ => finalize(tree, untpd.ContextBoundTypeTree(tycon, paramName)(tree.source)) def ImportSelector(tree: Tree)(imported: Ident, renamed: Tree, bound: Tree)(using Context): Tree = tree match { case tree: ImportSelector if (imported eq tree.imported) && (renamed eq tree.renamed) && (bound eq tree.bound) => tree case _ => finalize(tree, untpd.ImportSelector(imported, renamed, bound)(tree.source)) @@ -742,6 +746,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { cpy.PatDef(tree)(mods, transform(pats), transform(tpt), transform(rhs)) case ExtMethods(paramss, methods) => cpy.ExtMethods(tree)(transformParamss(paramss), transformSub(methods)) + case ContextBoundTypeTree(tycon, paramName) => + cpy.ContextBoundTypeTree(tree)(transform(tycon), paramName) case ImportSelector(imported, renamed, bound) => cpy.ImportSelector(tree)(transformSub(imported), transform(renamed), transform(bound)) case Number(_, _) | TypedSplice(_) => @@ -797,6 +803,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { this(this(this(x, pats), tpt), rhs) case ExtMethods(paramss, methods) => this(paramss.foldLeft(x)(apply), methods) + case ContextBoundTypeTree(tycon, paramName) => + this(x, tycon) case ImportSelector(imported, renamed, bound) => this(this(this(x, imported), renamed), bound) case Number(_, _) => diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 57a340453177..f21bf8278ea2 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2153,7 +2153,7 @@ object Parsers { def contextBounds(pname: TypeName): List[Tree] = if in.isColon then atSpan(in.skipToken()) { - AppliedTypeTree(toplevelTyp(), Ident(pname)) + ContextBoundTypeTree(toplevelTyp(), pname) } :: contextBounds(pname) else if in.token == VIEWBOUND then report.errorOrMigrationWarning( diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 93e280f8a13c..8efd0b21005a 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -373,7 +373,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { changePrec(GlobalPrec) { keywordStr("for ") ~ Text(enums map enumText, "; ") ~ sep ~ toText(expr) } def cxBoundToText(bound: untpd.Tree): Text = bound match { // DD - case AppliedTypeTree(tpt, _) => " : " ~ toText(tpt) + case ContextBoundTypeTree(tpt, _) => " : " ~ toText(tpt) case untpd.Function(_, tpt) => " <% " ~ toText(tpt) } @@ -776,6 +776,8 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { prefix ~~ idx.toString ~~ "|" ~~ tpeText ~~ "|" ~~ argsText ~~ "|" ~~ contentText ~~ postfix case CapturesAndResult(refs, parent) => changePrec(GlobalPrec)("^{" ~ Text(refs.map(toText), ", ") ~ "}" ~ toText(parent)) + case ContextBoundTypeTree(tycon, pname) => + toText(pname) ~ " is " ~ toText(tycon) case _ => tree.fallbackToText(this) } diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 13b24d704b05..14b66e0c374a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -1849,10 +1849,12 @@ class Namer { typer: Typer => val paramSymss = normalizeIfConstructor(ddef.paramss.nestedMap(symbolOfTree), isConstructor) sym.setParamss(paramSymss) - /** Set all class parameters with types that have an abstract type member - * to be tracked, provided they are `val` parameters, or context bound - * evidence parameters. In the second case, reset any private and local - * flags for context bound evidence parameter so that it becomes a `val`. + /** Set every context bound evidence parameter of a class to be tracked, + * provided it has a type that has an abstract type member. Reset private + * and local flags so that the parameter becomes a `val`. Do the same for all + * context bound evidence parameters of a `given` class. This is because + * in Desugar.addParamRefinements we create refinements for these parameters + * in the result type of the implicit access method. */ def setTracked(param: ValDef): Unit = val sym = symbolOfTree(param) @@ -1860,7 +1862,8 @@ class Namer { typer: Typer => case info: TempClassInfo => if !sym.is(Tracked) && param.hasAttachment(ContextBoundParam) - && sym.info.memberNames(abstractTypeNameFilter).nonEmpty + && (sym.info.memberNames(abstractTypeNameFilter).nonEmpty + || sym.maybeOwner.maybeOwner.is(Given)) then typr.println(i"set tracked $param, $sym: ${sym.info} containing ${sym.info.memberNames(abstractTypeNameFilter).toList}") for acc <- info.decls.lookupAll(sym.name) if acc.is(ParamAccessor) do diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 55282c597781..e701ea159ea7 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2204,6 +2204,19 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer tree.tpFun(tsyms, vsyms) completeTypeTree(InferredTypeTree(), tp, tree) + def typedContextBoundTypeTree(tree: untpd.ContextBoundTypeTree)(using Context): Tree = + val tycon = typedType(tree.tycon) + val tyconSplice = untpd.TypedSplice(tycon) + val tparam = untpd.Ident(tree.paramName).withSpan(tree.span) + if tycon.tpe.typeParams.nonEmpty then + typed(untpd.AppliedTypeTree(tyconSplice, tparam :: Nil)) + else if Feature.enabled(modularity) && tycon.tpe.member(tpnme.This).symbol.isAbstractType then + typed(untpd.RefinedTypeTree(tyconSplice, List(untpd.TypeDef(tpnme.This, tparam)))) + else + errorTree(tree, + em"""Illegal context bound: ${tycon.tpe} does not take type parameters and + does not have an abstract type member named `This` either.""") + def typedSingletonTypeTree(tree: untpd.SingletonTypeTree)(using Context): SingletonTypeTree = { val ref1 = typedExpr(tree.ref, SingletonTypeProto) checkStable(ref1.tpe, tree.srcPos, "singleton type") @@ -3193,6 +3206,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case tree: untpd.UnApply => typedUnApply(tree, pt) case tree: untpd.Tuple => typedTuple(tree, pt) case tree: untpd.InLambdaTypeTree => typedInLambdaTypeTree(tree, pt) + case tree: untpd.ContextBoundTypeTree => typedContextBoundTypeTree(tree) case tree: untpd.InfixOp => typedInfixOp(tree, pt) case tree: untpd.ParsedTry => typedTry(tree, pt) case tree @ untpd.PostfixOp(qual, Ident(nme.WILDCARD)) => typedAsFunction(tree, pt) diff --git a/compiler/test/dotc/pos-test-pickling.blacklist b/compiler/test/dotc/pos-test-pickling.blacklist index 53aa1c31aeea..05ea3395e579 100644 --- a/compiler/test/dotc/pos-test-pickling.blacklist +++ b/compiler/test/dotc/pos-test-pickling.blacklist @@ -121,3 +121,5 @@ i15525.scala # alias types at different levels of dereferencing parsercombinators-givens.scala parsercombinators-ctx-bounds.scala +parsercombinators-this.scala + diff --git a/tests/neg/i9330.scala b/tests/neg/i9330.scala index ca25582ef7e8..6ba57c033473 100644 --- a/tests/neg/i9330.scala +++ b/tests/neg/i9330.scala @@ -1,4 +1,4 @@ val x = { - () == "" // error + () == "" implicit def foo[A: A] // error // error // error } diff --git a/tests/pos/parsercombinators-this.scala b/tests/pos/parsercombinators-this.scala new file mode 100644 index 000000000000..6a38613b4ec6 --- /dev/null +++ b/tests/pos/parsercombinators-this.scala @@ -0,0 +1,53 @@ +//> using options -language:experimental.modularity -source future +import collection.mutable + +/// A parser combinator. +trait Combinator: + + type This + + /// The context from which elements are being parsed, typically a stream of tokens. + type Context + /// The element being parsed. + type Element + + extension (self: This) + /// Parses and returns an element from `context`. + def parse(context: Context): Option[Element] +end Combinator + +final case class Apply[C, E](action: C => Option[E]) +final case class Combine[A, B](first: A, second: B) + +given apply[C, E]: Combinator with { + type This = Apply[C, E] + type Context = C + type Element = E + extension(self: Apply[C, E]) { + def parse(context: C): Option[E] = self.action(context) + } +} + +given combine[A: Combinator, B: Combinator { type Context = A.Context }] + : Combinator with + type This = Combine[A, B] + type Context = A.Context + type Element = (A.Element, B.Element) + extension(self: Combine[A, B]) + def parse(context: Context): Option[Element] = ??? + +extension [A] (buf: mutable.ListBuffer[A]) def popFirst() = + if buf.isEmpty then None + else try Some(buf.head) finally buf.remove(0) + +@main def hello: Unit = { + val source = (0 to 10).toList + val stream = source.to(mutable.ListBuffer) + + val n = Apply[mutable.ListBuffer[Int], Int](s => s.popFirst()) + val m = Combine(n, n) + + val r = m.parse(stream) // error: type mismatch, found `mutable.ListBuffer[Int]`, required `?1.Context` + val rc: Option[(Int, Int)] = r + // it would be great if this worked +} diff --git a/tests/pos/typeclasses-this.scala b/tests/pos/typeclasses-this.scala new file mode 100644 index 000000000000..b6e1cf70845e --- /dev/null +++ b/tests/pos/typeclasses-this.scala @@ -0,0 +1,144 @@ +//> using options -language:experimental.modularity -source future + +// this should go in Predef +infix type is[A <: AnyKind, B <: {type This <: AnyKind}] = B { type This = A } + +class Common: + + trait Ord: + type This + extension (x: This) + def compareTo(y: This): Int + def < (y: This): Boolean = compareTo(y) < 0 + def > (y: This): Boolean = compareTo(y) > 0 + def <= (y: This): Boolean = compareTo(y) <= 0 + def >= (y: This): Boolean = compareTo(y) >= 0 + def max(y: This): This = if x < y then y else x + + trait Show: + type This + extension (x: This) def show: String + + trait SemiGroup: + type This + extension (x: This) def combine(y: This): This + + trait Monoid extends SemiGroup: + def unit: This + + trait Functor: + type This[A] + extension [A](x: This[A]) def map[B](f: A => B): This[B] + + trait Monad extends Functor: + def pure[A](x: A): This[A] + extension [A](x: This[A]) + def flatMap[B](f: A => This[B]): This[B] + def map[B](f: A => B) = x.flatMap(f `andThen` pure) +end Common + +object Instances extends Common: + + given intOrd: (Int is Ord) with + extension (x: Int) + def compareTo(y: Int) = + if x < y then -1 + else if x > y then +1 + else 0 + +// given [T](using tracked val ev: Ord { type This = T}): Ord { type This = List[T] } with + given [T: Ord]: (List[T] is Ord) with + extension (xs: List[T]) def compareTo(ys: List[T]): Int = (xs, ys) match + case (Nil, Nil) => 0 + case (Nil, _) => -1 + case (_, Nil) => +1 + case (x :: xs1, y :: ys1) => + val fst = x.compareTo(y) + if (fst != 0) fst else xs1.compareTo(ys1) + + given listMonad: (List is Monad) with + extension [A](xs: List[A]) def flatMap[B](f: A => List[B]): List[B] = + xs.flatMap(f) + def pure[A](x: A): List[A] = + List(x) + + type Reader[Ctx] = [X] =>> Ctx => X + + given readerMonad[Ctx]: (Reader[Ctx] is Monad) with + extension [A](r: Ctx => A) def flatMap[B](f: A => Ctx => B): Ctx => B = + ctx => f(r(ctx))(ctx) + def pure[A](x: A): Ctx => A = + ctx => x + + extension (xs: Seq[String]) + def longestStrings: Seq[String] = + val maxLength = xs.map(_.length).max + xs.filter(_.length == maxLength) + + extension [T](xs: List[T]) + def second = xs.tail.head + def third = xs.tail.tail.head +/* + extension [M[_]: Monad, A](xss: M[M[A]]) + def flatten: M[A] = + xss.flatMap(identity) +*/ + def maximum[T: Ord](xs: List[T]): T = + xs.reduce(_ `max` _) + + given descending[T: Ord]: (T is Ord) with + extension (x: T) def compareTo(y: T) = T.compareTo(y)(x) + + def minimum[T: Ord](xs: List[T]) = + maximum(xs)(using descending) + + def test(): Unit = + val xs = List(1, 2, 3) + println(maximum(xs)) + println(maximum(xs)(using descending)) + println(maximum(xs)(using descending(using intOrd))) + println(minimum(xs)) + +// Adapted from the Rust by Example book: https://doc.rust-lang.org/rust-by-example/trait.html +// +// lines words chars +// wc Scala: 28 105 793 +// wc Rust : 57 193 1466 + +trait Animal: + type This + // Associated function signature; `This` refers to the implementor type. + def apply(name: String): This + + // Method signatures; these will return a string. + extension (self: This) + def name: String + def noise: String + def talk(): Unit = println(s"$name, $noise") +end Animal + +class Sheep(val name: String): + var isNaked = false + def shear() = + if isNaked then + println(s"$name is already naked...") + else + println(s"$name gets a haircut!") + isNaked = true + +given (Sheep is Animal) with + def apply(name: String) = Sheep(name) + extension (self: This) + def name: String = self.name + def noise: String = if self.isNaked then "baaaaah?" else "baaaaah!" + override def talk(): Unit = + println(s"$name pauses briefly... $noise") + +/* + + - In a type pattern, A <: T, A >: T, A: T, A: _ are all allowed and mean + T is a fresh type variable (T can start with a capital letter). + - instance definitions + - `as m` syntax in context bounds and instance definitions + +*/ From 84aa459b0f9edc749196b4dc3305d2c4c41dcd5a Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Jan 2024 21:22:04 +0100 Subject: [PATCH 11/63] Drop restriction against typedefs at level * only Allow the RHS of a type def to be higher-kinded. But keep the restriction for opaque type aliases; their RHS must be fully applied. I am not sure why the restriction applies to them, but there was a test specifically about that, so there night be a reason. # Conflicts: # compiler/src/dotty/tools/dotc/typer/Typer.scala --- .../src/dotty/tools/dotc/typer/Checking.scala | 16 ++++++++-------- compiler/src/dotty/tools/dotc/typer/Typer.scala | 7 ++++--- tests/neg/i12456.scala | 2 +- tests/neg/i13757-match-type-anykind.scala | 2 +- tests/neg/i9328.scala | 2 +- tests/neg/parser-stability-12.scala | 2 +- tests/neg/unapplied-types.scala | 7 ------- tests/pos/typeclasses-this.scala | 4 ++-- tests/pos/unapplied-types.scala | 7 +++++++ 9 files changed, 25 insertions(+), 24 deletions(-) delete mode 100644 tests/neg/unapplied-types.scala create mode 100644 tests/pos/unapplied-types.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 1bd0d4035758..edb66e829c6b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -1322,20 +1322,20 @@ trait Checking { } /** Check that user-defined (result) type is fully applied */ - def checkFullyAppliedType(tree: Tree)(using Context): Unit = tree match + def checkFullyAppliedType(tree: Tree, prefix: String)(using Context): Unit = tree match case TypeBoundsTree(lo, hi, alias) => - checkFullyAppliedType(lo) - checkFullyAppliedType(hi) - checkFullyAppliedType(alias) + checkFullyAppliedType(lo, prefix) + checkFullyAppliedType(hi, prefix) + checkFullyAppliedType(alias, prefix) case Annotated(arg, annot) => - checkFullyAppliedType(arg) + checkFullyAppliedType(arg, prefix) case LambdaTypeTree(_, body) => - checkFullyAppliedType(body) + checkFullyAppliedType(body, prefix) case _: TypeTree => case _ => if tree.tpe.typeParams.nonEmpty then val what = if tree.symbol.exists then tree.symbol.show else i"type $tree" - report.error(em"$what takes type parameters", tree.srcPos) + report.error(em"$prefix$what takes type parameters", tree.srcPos) /** Check that we are in an inline context (inside an inline method or in inline code) */ def checkInInlineContext(what: String, pos: SrcPos)(using Context): Unit = @@ -1600,7 +1600,7 @@ trait ReChecking extends Checking { override def checkEnumParent(cls: Symbol, firstParent: Symbol)(using Context): Unit = () override def checkEnum(cdef: untpd.TypeDef, cls: Symbol, firstParent: Symbol)(using Context): Unit = () override def checkRefsLegal(tree: tpd.Tree, badOwner: Symbol, allowed: (Name, Symbol) => Boolean, where: String)(using Context): Unit = () - override def checkFullyAppliedType(tree: Tree)(using Context): Unit = () + override def checkFullyAppliedType(tree: Tree, prefix: String)(using Context): Unit = () override def checkEnumCaseRefsLegal(cdef: TypeDef, enumCtx: Context)(using Context): Unit = () override def checkAnnotApplicable(annot: Tree, sym: Symbol)(using Context): Boolean = true override def checkMatchable(tp: Type, pos: SrcPos, pattern: Boolean)(using Context): Unit = () diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index e701ea159ea7..f4ea1c587001 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2215,7 +2215,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer else errorTree(tree, em"""Illegal context bound: ${tycon.tpe} does not take type parameters and - does not have an abstract type member named `This` either.""") + |does not have an abstract type member named `This` either.""") def typedSingletonTypeTree(tree: untpd.SingletonTypeTree)(using Context): SingletonTypeTree = { val ref1 = typedExpr(tree.ref, SingletonTypeProto) @@ -2713,8 +2713,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer typeIndexedLambdaTypeTree(rhs, tparams, body) case rhs => typedType(rhs) - checkFullyAppliedType(rhs1) - if sym.isOpaqueAlias then checkNoContextFunctionType(rhs1) + if sym.isOpaqueAlias then + checkFullyAppliedType(rhs1, "Opaque type alias must be fully applied, but ") + checkNoContextFunctionType(rhs1) assignType(cpy.TypeDef(tdef)(name, rhs1), sym) } diff --git a/tests/neg/i12456.scala b/tests/neg/i12456.scala index b9fb0283dcd7..c1a3ada5a420 100644 --- a/tests/neg/i12456.scala +++ b/tests/neg/i12456.scala @@ -1 +1 @@ -object F { type T[G[X] <: X, F <: G[F]] } // error // error +object F { type T[G[X] <: X, F <: G[F]] } // error diff --git a/tests/neg/i13757-match-type-anykind.scala b/tests/neg/i13757-match-type-anykind.scala index a80e8b2b289b..998c54292b15 100644 --- a/tests/neg/i13757-match-type-anykind.scala +++ b/tests/neg/i13757-match-type-anykind.scala @@ -8,7 +8,7 @@ object Test: type AnyKindMatchType3[X <: AnyKind] = X match // error: the scrutinee of a match type cannot be higher-kinded case _ => Int - type AnyKindMatchType4[X <: Option] = X match // error // error: the scrutinee of a match type cannot be higher-kinded + type AnyKindMatchType4[X <: Option] = X match // error: the scrutinee of a match type cannot be higher-kinded case _ => Int type AnyKindMatchType5[X[_]] = X match // error: the scrutinee of a match type cannot be higher-kinded diff --git a/tests/neg/i9328.scala b/tests/neg/i9328.scala index dabde498e1dc..c13d33e103b9 100644 --- a/tests/neg/i9328.scala +++ b/tests/neg/i9328.scala @@ -3,7 +3,7 @@ type Id[T] = T match { case _ => T } -class Foo2[T <: Id[T]] // error // error +class Foo2[T <: Id[T]] // error object Foo { // error object Foo { } diff --git a/tests/neg/parser-stability-12.scala b/tests/neg/parser-stability-12.scala index 78ff178d010c..17a611d70e34 100644 --- a/tests/neg/parser-stability-12.scala +++ b/tests/neg/parser-stability-12.scala @@ -1,4 +1,4 @@ trait x0[]: // error - trait x1[x1 <:x0] // error: type x0 takes type parameters + trait x1[x1 <:x0] extends x1[ // error // error \ No newline at end of file diff --git a/tests/neg/unapplied-types.scala b/tests/neg/unapplied-types.scala deleted file mode 100644 index 2f2339baa026..000000000000 --- a/tests/neg/unapplied-types.scala +++ /dev/null @@ -1,7 +0,0 @@ -trait T { - type L[X] = List[X] - type T1 <: L // error: takes type parameters - type T2 = L // error: takes type parameters - type T3 = List // error: takes type parameters - type T4 <: List // error: takes type parameters -} diff --git a/tests/pos/typeclasses-this.scala b/tests/pos/typeclasses-this.scala index b6e1cf70845e..e169bebbc078 100644 --- a/tests/pos/typeclasses-this.scala +++ b/tests/pos/typeclasses-this.scala @@ -78,11 +78,11 @@ object Instances extends Common: extension [T](xs: List[T]) def second = xs.tail.head def third = xs.tail.tail.head -/* + extension [M[_]: Monad, A](xss: M[M[A]]) def flatten: M[A] = xss.flatMap(identity) -*/ + def maximum[T: Ord](xs: List[T]): T = xs.reduce(_ `max` _) diff --git a/tests/pos/unapplied-types.scala b/tests/pos/unapplied-types.scala new file mode 100644 index 000000000000..604e63deb8ad --- /dev/null +++ b/tests/pos/unapplied-types.scala @@ -0,0 +1,7 @@ +trait T { + type L[X] = List[X] + type T1 <: L // was error: takes type parameters + type T2 = L // was error: takes type parameters + type T3 = List // was error: takes type parameters + type T4 <: List // was error: takes type parameters +} From 78018a8704b21f42b004d33ec60c0b8557f981d1 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Jan 2024 21:30:17 +0100 Subject: [PATCH 12/63] Allow types in given definitions to be infix types A type implemented in a given definition can now be an infix type, without enclosing parens being necessary. By contrast, it cannot anymore be a refined type. Refined types have to be enclosed in parens. This second point aligns the dotty parser with the published syntax and the scala meta parser. --- .../dotty/tools/dotc/parsing/Parsers.scala | 26 +++++++++++++------ docs/_docs/internals/syntax.md | 4 ++- docs/_docs/reference/syntax.md | 9 ++++--- tests/neg/i12348.check | 16 ++++++------ tests/neg/i12348.scala | 3 +-- tests/neg/i7045.scala | 7 +++++ tests/pos/i7045.scala | 9 ------- tests/pos/typeclass-aggregates.scala | 6 ++--- tests/pos/typeclasses-this.scala | 12 ++++----- 9 files changed, 51 insertions(+), 41 deletions(-) create mode 100644 tests/neg/i7045.scala delete mode 100644 tests/pos/i7045.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index f21bf8278ea2..cce62169f419 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1771,8 +1771,8 @@ object Parsers { */ def infixType(): Tree = infixTypeRest(refinedType()) - def infixTypeRest(t: Tree): Tree = - infixOps(t, canStartInfixTypeTokens, refinedTypeFn, Location.ElseWhere, ParseKind.Type, + def infixTypeRest(t: Tree, operand: Location => Tree = refinedTypeFn): Tree = + infixOps(t, canStartInfixTypeTokens, operand, Location.ElseWhere, ParseKind.Type, isOperator = !followingIsVararg() && !isPureArrow && nextCanFollowOperator(canStartInfixTypeTokens)) @@ -1837,6 +1837,10 @@ object Parsers { */ def annotType(): Tree = annotTypeRest(simpleType()) + /** AnnotType1 ::= SimpleType1 {Annotation} + */ + def annotType1(): Tree = annotTypeRest(simpleType1()) + def annotTypeRest(t: Tree): Tree = if (in.token == AT) annotTypeRest(atSpan(startOffset(t)) { @@ -4055,8 +4059,10 @@ object Parsers { syntaxError(em"extension clause can only define methods", stat.span) } - /** GivenDef ::= [GivenSig] (AnnotType [‘=’ Expr] | StructuralInstance) - * GivenSig ::= [id] [DefTypeParamClause] {UsingParamClauses} ‘:’ + /** GivenDef ::= [GivenSig] (GivenType [‘=’ Expr] | StructuralInstance) + * GivenSig ::= [id] [DefTypeParamClause] {UsingParamClauses} ‘:’ + * GivenType ::= AnnotType1 {id [nl] AnnotType1} + * StructuralInstance ::= ConstrApp {‘with’ ConstrApp} [‘with’ WithTemplateBody] */ def givenDef(start: Offset, mods: Modifiers, givenMod: Mod) = atSpan(start, nameStart) { var mods1 = addMod(mods, givenMod) @@ -4082,8 +4088,12 @@ object Parsers { val noParams = tparams.isEmpty && vparamss.isEmpty if !(name.isEmpty && noParams) then acceptColon() val parents = - if isSimpleLiteral then rejectWildcardType(annotType()) :: Nil - else refinedTypeRest(constrApp()) :: withConstrApps() + if isSimpleLiteral then + rejectWildcardType(annotType()) :: Nil + else constrApp() match + case parent: Apply => parent :: withConstrApps() + case parent if in.isIdent => infixTypeRest(parent, _ => annotType1()) :: Nil + case parent => parent :: withConstrApps() val parentsIsType = parents.length == 1 && parents.head.isType if in.token == EQUALS && parentsIsType then accept(EQUALS) @@ -4177,10 +4187,10 @@ object Parsers { /* -------- TEMPLATES ------------------------------------------- */ - /** ConstrApp ::= SimpleType1 {Annotation} {ParArgumentExprs} + /** ConstrApp ::= AnnotType1 {ParArgumentExprs} */ val constrApp: () => Tree = () => - val t = rejectWildcardType(annotTypeRest(simpleType1()), + val t = rejectWildcardType(annotType1(), fallbackTree = Ident(tpnme.ERROR)) // Using Ident(tpnme.ERROR) to avoid causing cascade errors on non-user-written code if in.token == LPAREN then parArgumentExprss(wrapNew(t)) else t diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index d2a2bd6de80a..ed00eec3a612 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -191,6 +191,7 @@ MatchType ::= InfixType `match` <<< TypeCaseClauses >>> InfixType ::= RefinedType {id [nl] RefinedType} InfixOp(t1, op, t2) RefinedType ::= AnnotType {[nl] Refinement} RefinedTypeTree(t, ds) AnnotType ::= SimpleType {Annotation} Annotated(t, annot) +AnnotType1 ::= SimpleType1 {Annotation} Annotated(t, annot) SimpleType ::= SimpleLiteral SingletonTypeTree(l) | ‘?’ TypeBounds @@ -458,8 +459,9 @@ ClassConstr ::= [ClsTypeParamClause] [ConstrMods] ClsParamClauses ConstrMods ::= {Annotation} [AccessModifier] ObjectDef ::= id [Template] ModuleDef(mods, name, template) // no constructor EnumDef ::= id ClassConstr InheritClauses EnumBody -GivenDef ::= [GivenSig] (AnnotType [‘=’ Expr] | StructuralInstance) +GivenDef ::= [GivenSig] (GivenType [‘=’ Expr] | StructuralInstance) GivenSig ::= [id] [DefTypeParamClause] {UsingParamClause} ‘:’ -- one of `id`, `DefTypeParamClause`, `UsingParamClause` must be present +GivenType ::= AnnotType1 {id [nl] AnnotType1} StructuralInstance ::= ConstrApp {‘with’ ConstrApp} [‘with’ WithTemplateBody] Extension ::= ‘extension’ [DefTypeParamClause] {UsingParamClause} ‘(’ DefTermParam ‘)’ {UsingParamClause} ExtMethods diff --git a/docs/_docs/reference/syntax.md b/docs/_docs/reference/syntax.md index 1980bc4e0ab2..b07bc174162e 100644 --- a/docs/_docs/reference/syntax.md +++ b/docs/_docs/reference/syntax.md @@ -200,8 +200,8 @@ SimpleType ::= SimpleLiteral | Singleton ‘.’ ‘type’ | ‘(’ Types ‘)’ | Refinement - | SimpleType1 TypeArgs - | SimpleType1 ‘#’ id + | SimpleType TypeArgs + | SimpleType ‘#’ id Singleton ::= SimpleRef | SimpleLiteral | Singleton ‘.’ id @@ -392,7 +392,7 @@ LocalModifier ::= ‘abstract’ AccessModifier ::= (‘private’ | ‘protected’) [AccessQualifier] AccessQualifier ::= ‘[’ id ‘]’ -Annotation ::= ‘@’ SimpleType1 {ParArgumentExprs} +Annotation ::= ‘@’ SimpleType {ParArgumentExprs} Import ::= ‘import’ ImportExpr {‘,’ ImportExpr} Export ::= ‘export’ ImportExpr {‘,’ ImportExpr} @@ -443,6 +443,7 @@ ObjectDef ::= id [Template] EnumDef ::= id ClassConstr InheritClauses EnumBody GivenDef ::= [GivenSig] (AnnotType [‘=’ Expr] | StructuralInstance) GivenSig ::= [id] [DefTypeParamClause] {UsingParamClause} ‘:’ -- one of `id`, `DefTypeParamClause`, `UsingParamClause` must be present +GivenType ::= AnnotType {id [nl] AnnotType} StructuralInstance ::= ConstrApp {‘with’ ConstrApp} [‘with’ WithTemplateBody] Extension ::= ‘extension’ [DefTypeParamClause] {UsingParamClause} ‘(’ DefTermParam ‘)’ {UsingParamClause} ExtMethods @@ -452,7 +453,7 @@ ExtMethod ::= {Annotation [nl]} {Modifier} ‘def’ DefDef Template ::= InheritClauses [TemplateBody] InheritClauses ::= [‘extends’ ConstrApps] [‘derives’ QualId {‘,’ QualId}] ConstrApps ::= ConstrApp ({‘,’ ConstrApp} | {‘with’ ConstrApp}) -ConstrApp ::= SimpleType1 {Annotation} {ParArgumentExprs} +ConstrApp ::= SimpleType {Annotation} {ParArgumentExprs} ConstrExpr ::= SelfInvocation | <<< SelfInvocation {semi BlockStat} >>> SelfInvocation ::= ‘this’ ArgumentExprs {ArgumentExprs} diff --git a/tests/neg/i12348.check b/tests/neg/i12348.check index ccc2b9f7ed00..eded51f70f31 100644 --- a/tests/neg/i12348.check +++ b/tests/neg/i12348.check @@ -1,8 +1,8 @@ --- [E040] Syntax Error: tests/neg/i12348.scala:2:15 -------------------------------------------------------------------- -2 | given inline x: Int = 0 // error - | ^ - | 'with' expected, but identifier found --- [E040] Syntax Error: tests/neg/i12348.scala:3:10 -------------------------------------------------------------------- -3 |} // error - | ^ - | '}' expected, but eof found +-- [E040] Syntax Error: tests/neg/i12348.scala:2:16 -------------------------------------------------------------------- +2 | given inline x: Int = 0 // error // error + | ^ + | an identifier expected, but ':' found +-- [E067] Syntax Error: tests/neg/i12348.scala:2:8 --------------------------------------------------------------------- +2 | given inline x: Int = 0 // error // error + | ^ + |Declaration of given instance given_x_inline_ not allowed here: only classes can have declared but undefined members diff --git a/tests/neg/i12348.scala b/tests/neg/i12348.scala index 69fc77fb532e..43daf9a2801b 100644 --- a/tests/neg/i12348.scala +++ b/tests/neg/i12348.scala @@ -1,3 +1,2 @@ object A { - given inline x: Int = 0 // error -} // error \ No newline at end of file + given inline x: Int = 0 // error // error diff --git a/tests/neg/i7045.scala b/tests/neg/i7045.scala new file mode 100644 index 000000000000..b4c6d60cd35a --- /dev/null +++ b/tests/neg/i7045.scala @@ -0,0 +1,7 @@ +trait Bar { type Y } +trait Foo { type X } + +class Test: + given a1(using b: Bar): Foo = new Foo { type X = b.Y } // ok + given a2(using b: Bar): (Foo { type X = b.Y }) = new Foo { type X = b.Y } // ok + given a3(using b: Bar): Foo { type X = b.Y } = new Foo { type X = b.Y } // error \ No newline at end of file diff --git a/tests/pos/i7045.scala b/tests/pos/i7045.scala deleted file mode 100644 index e683654dd5c3..000000000000 --- a/tests/pos/i7045.scala +++ /dev/null @@ -1,9 +0,0 @@ -trait Bar { type Y } -trait Foo { type X } - -class Test: - given a1(using b: Bar): Foo = new Foo { type X = b.Y } - - given a2(using b: Bar): Foo { type X = b.Y } = new Foo { type X = b.Y } - - given a3(using b: Bar): (Foo { type X = b.Y }) = new Foo { type X = b.Y } diff --git a/tests/pos/typeclass-aggregates.scala b/tests/pos/typeclass-aggregates.scala index 77b0f1a9f04a..9bb576603b7b 100644 --- a/tests/pos/typeclass-aggregates.scala +++ b/tests/pos/typeclass-aggregates.scala @@ -30,8 +30,8 @@ trait OrdWithMonoid extends Ord, Monoid def ordWithMonoid2(ord: Ord, monoid: Monoid{ type This = ord.This }) = //: OrdWithMonoid { type This = ord.This} = new OrdWithMonoid with ord.OrdProxy with monoid.MonoidProxy {} -given intOrd: Ord { type This = Int } = ??? -given intMonoid: Monoid { type This = Int } = ??? +given intOrd: (Ord { type This = Int }) = ??? +given intMonoid: (Monoid { type This = Int }) = ??? //given (using ord: Ord, monoid: Monoid{ type This = ord.This }): (Ord & Monoid { type This = ord.This}) = // ordWithMonoid2(ord, monoid) @@ -42,6 +42,6 @@ val y: Int = ??? : x.This // given [A, B](using ord: A is Ord, monoid: A is Monoid) => A is Ord & Monoid = // new ord.OrdProxy with monoid.MonoidProxy {} -given [A](using ord: Ord { type This = A }, monoid: Monoid { type This = A}): (Ord & Monoid) { type This = A} = +given [A](using ord: Ord { type This = A }, monoid: Monoid { type This = A}): ((Ord & Monoid) { type This = A}) = new ord.OrdProxy with monoid.MonoidProxy {} diff --git a/tests/pos/typeclasses-this.scala b/tests/pos/typeclasses-this.scala index e169bebbc078..7fb3e553fea7 100644 --- a/tests/pos/typeclasses-this.scala +++ b/tests/pos/typeclasses-this.scala @@ -39,7 +39,7 @@ end Common object Instances extends Common: - given intOrd: (Int is Ord) with + given intOrd: Int is Ord with extension (x: Int) def compareTo(y: Int) = if x < y then -1 @@ -47,7 +47,7 @@ object Instances extends Common: else 0 // given [T](using tracked val ev: Ord { type This = T}): Ord { type This = List[T] } with - given [T: Ord]: (List[T] is Ord) with + given [T: Ord]: List[T] is Ord with extension (xs: List[T]) def compareTo(ys: List[T]): Int = (xs, ys) match case (Nil, Nil) => 0 case (Nil, _) => -1 @@ -56,7 +56,7 @@ object Instances extends Common: val fst = x.compareTo(y) if (fst != 0) fst else xs1.compareTo(ys1) - given listMonad: (List is Monad) with + given listMonad: List is Monad with extension [A](xs: List[A]) def flatMap[B](f: A => List[B]): List[B] = xs.flatMap(f) def pure[A](x: A): List[A] = @@ -64,7 +64,7 @@ object Instances extends Common: type Reader[Ctx] = [X] =>> Ctx => X - given readerMonad[Ctx]: (Reader[Ctx] is Monad) with + given readerMonad[Ctx]: Reader[Ctx] is Monad with extension [A](r: Ctx => A) def flatMap[B](f: A => Ctx => B): Ctx => B = ctx => f(r(ctx))(ctx) def pure[A](x: A): Ctx => A = @@ -86,7 +86,7 @@ object Instances extends Common: def maximum[T: Ord](xs: List[T]): T = xs.reduce(_ `max` _) - given descending[T: Ord]: (T is Ord) with + given descending[T: Ord]: T is Ord with extension (x: T) def compareTo(y: T) = T.compareTo(y)(x) def minimum[T: Ord](xs: List[T]) = @@ -126,7 +126,7 @@ class Sheep(val name: String): println(s"$name gets a haircut!") isNaked = true -given (Sheep is Animal) with +given Sheep is Animal with def apply(name: String) = Sheep(name) extension (self: This) def name: String = self.name From 093ac5d7a11ffae659bb84927b195c5f7a524bd3 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Jan 2024 21:43:02 +0100 Subject: [PATCH 13/63] New syntax for given defs given [A: Ord] => A is Ord: ... given [A: Ord] => A is Ord as listOrd: ... --- .../dotty/tools/dotc/parsing/Parsers.scala | 67 ++++++-- .../test/dotc/pos-test-pickling.blacklist | 2 +- docs/_docs/internals/syntax.md | 9 +- tests/pos/parsercombinators-arrow.scala | 50 ++++++ tests/pos/typeclasses-arrow.scala | 143 ++++++++++++++++++ 5 files changed, 255 insertions(+), 16 deletions(-) create mode 100644 tests/pos/parsercombinators-arrow.scala create mode 100644 tests/pos/typeclasses-arrow.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index cce62169f419..6246b468d3eb 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -948,12 +948,14 @@ object Parsers { * i.e. an identifier followed by type and value parameters, followed by `:`? * @pre The current token is an identifier */ - def followingIsGivenSig() = + def followingIsOldStyleGivenSig() = val lookahead = in.LookaheadScanner() if lookahead.isIdent then lookahead.nextToken() + var paramsSeen = false def skipParams(): Unit = if lookahead.token == LPAREN || lookahead.token == LBRACKET then + paramsSeen = true lookahead.skipParens() skipParams() else if lookahead.isNewLine then @@ -961,6 +963,17 @@ object Parsers { skipParams() skipParams() lookahead.isColon + && { + !in.featureEnabled(Feature.modularity) + || paramsSeen + || { // with modularity language import, a `:` at EOL after an identifier represents a single identifier given + // Example: + // given C: + // def f = ... + lookahead.nextToken() + !lookahead.isAfterLineEnd + } + } def followingIsExtension() = val next = in.lookahead.token @@ -1773,7 +1786,7 @@ object Parsers { def infixTypeRest(t: Tree, operand: Location => Tree = refinedTypeFn): Tree = infixOps(t, canStartInfixTypeTokens, operand, Location.ElseWhere, ParseKind.Type, - isOperator = !followingIsVararg() && !isPureArrow + isOperator = !followingIsVararg() && !isPureArrow && !isIdent(nme.as) && nextCanFollowOperator(canStartInfixTypeTokens)) /** RefinedType ::= WithType {[nl] Refinement} [`^` CaptureSet] @@ -4059,15 +4072,23 @@ object Parsers { syntaxError(em"extension clause can only define methods", stat.span) } - /** GivenDef ::= [GivenSig] (GivenType [‘=’ Expr] | StructuralInstance) - * GivenSig ::= [id] [DefTypeParamClause] {UsingParamClauses} ‘:’ - * GivenType ::= AnnotType1 {id [nl] AnnotType1} + /** GivenDef ::= OldGivenDef | NewGivenDef + * OldGivenDef ::= [OldGivenSig] (GivenType [‘=’ Expr] | StructuralInstance) + * OldGivenSig ::= [id] [DefTypeParamClause] {UsingParamClauses} ‘:’ * StructuralInstance ::= ConstrApp {‘with’ ConstrApp} [‘with’ WithTemplateBody] + * + * NewGivenDef ::= [GivenConditional '=>'] NewGivenSig + * GivenConditional ::= [DefTypeParamClause | UsingParamClause] {UsingParamClause} + * NewGivenSig ::= GivenType ['as' id] ([‘=’ Expr] | TemplateBody) + * | ConstrApps ['as' id] TemplateBody + * + * GivenType ::= AnnotType1 {id [nl] AnnotType1} */ def givenDef(start: Offset, mods: Modifiers, givenMod: Mod) = atSpan(start, nameStart) { var mods1 = addMod(mods, givenMod) val nameStart = in.offset - val name = if isIdent && followingIsGivenSig() then ident() else EmptyTermName + var name = if isIdent && followingIsOldStyleGivenSig() then ident() else EmptyTermName + var newSyntaxAllowed = in.featureEnabled(Feature.modularity) // TODO Change syntax description def adjustDefParams(paramss: List[ParamClause]): List[ParamClause] = @@ -4077,6 +4098,13 @@ object Parsers { param.withMods(param.mods &~ (AccessFlags | ParamAccessor | Tracked | Mutable) | Param) .asInstanceOf[List[ParamClause]] + def moreConstrApps() = + if newSyntaxAllowed && in.token == COMMA then + in.nextToken() + constrApps() + else // need to be careful with last `with` + withConstrApps() + val gdef = val tparams = typeParamClauseOpt(ParamOwner.Given) newLineOpt() @@ -4086,14 +4114,24 @@ object Parsers { else Nil newLinesOpt() val noParams = tparams.isEmpty && vparamss.isEmpty - if !(name.isEmpty && noParams) then acceptColon() + if !(name.isEmpty && noParams) then + if in.isColon then + newSyntaxAllowed = false + in.nextToken() + else if newSyntaxAllowed then accept(ARROW) + else acceptColon() val parents = if isSimpleLiteral then rejectWildcardType(annotType()) :: Nil else constrApp() match - case parent: Apply => parent :: withConstrApps() - case parent if in.isIdent => infixTypeRest(parent, _ => annotType1()) :: Nil - case parent => parent :: withConstrApps() + case parent: Apply => parent :: moreConstrApps() + case parent if in.isIdent => + infixTypeRest(parent, _ => annotType1()) :: Nil + case parent => parent :: moreConstrApps() + if newSyntaxAllowed && in.isIdent(nme.as) then + in.nextToken() + name = ident() + val parentsIsType = parents.length == 1 && parents.head.isType if in.token == EQUALS && parentsIsType then accept(EQUALS) @@ -4114,8 +4152,13 @@ object Parsers { else vparam val constr = makeConstructor(tparams, vparamss1) val templ = - if isStatSep || isStatSeqEnd then Template(constr, parents, Nil, EmptyValDef, Nil) - else withTemplate(constr, parents) + if isStatSep || isStatSeqEnd then + Template(constr, parents, Nil, EmptyValDef, Nil) + else if !newSyntaxAllowed || in.token == WITH then + withTemplate(constr, parents) + else + possibleTemplateStart() + templateBodyOpt(constr, parents, Nil) if noParams && !mods.is(Inline) then ModuleDef(name, templ) else TypeDef(name.toTypeName, templ) end gdef diff --git a/compiler/test/dotc/pos-test-pickling.blacklist b/compiler/test/dotc/pos-test-pickling.blacklist index 05ea3395e579..0297d2651e4b 100644 --- a/compiler/test/dotc/pos-test-pickling.blacklist +++ b/compiler/test/dotc/pos-test-pickling.blacklist @@ -122,4 +122,4 @@ i15525.scala parsercombinators-givens.scala parsercombinators-ctx-bounds.scala parsercombinators-this.scala - +parsercombinators-arrow.scala diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index ed00eec3a612..0ef7de343278 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -459,10 +459,13 @@ ClassConstr ::= [ClsTypeParamClause] [ConstrMods] ClsParamClauses ConstrMods ::= {Annotation} [AccessModifier] ObjectDef ::= id [Template] ModuleDef(mods, name, template) // no constructor EnumDef ::= id ClassConstr InheritClauses EnumBody -GivenDef ::= [GivenSig] (GivenType [‘=’ Expr] | StructuralInstance) -GivenSig ::= [id] [DefTypeParamClause] {UsingParamClause} ‘:’ -- one of `id`, `DefTypeParamClause`, `UsingParamClause` must be present + +GivenDef ::= [GivenConditional '=>'] GivenSig +GivenConditional ::= [DefTypeParamClause | UsingParamClause] {UsingParamClause} +GivenSig ::= GivenType ['as' id] ([‘=’ Expr] | TemplateBody) + | ConstrApps ['as' id] TemplateBody GivenType ::= AnnotType1 {id [nl] AnnotType1} -StructuralInstance ::= ConstrApp {‘with’ ConstrApp} [‘with’ WithTemplateBody] + Extension ::= ‘extension’ [DefTypeParamClause] {UsingParamClause} ‘(’ DefTermParam ‘)’ {UsingParamClause} ExtMethods ExtMethods ::= ExtMethod | [nl] <<< ExtMethod {semi ExtMethod} >>> diff --git a/tests/pos/parsercombinators-arrow.scala b/tests/pos/parsercombinators-arrow.scala new file mode 100644 index 000000000000..b2d85aa1d49c --- /dev/null +++ b/tests/pos/parsercombinators-arrow.scala @@ -0,0 +1,50 @@ +//> using options -language:experimental.modularity -source future +import collection.mutable + +infix type is[A <: AnyKind, B <: {type This <: AnyKind}] = B { type This = A } + +/// A parser combinator. +trait Combinator: + + type This + + /// The context from which elements are being parsed, typically a stream of tokens. + type Context + /// The element being parsed. + type Element + + extension (self: This) + /// Parses and returns an element from `context`. + def parse(context: Context): Option[Element] +end Combinator + +final case class Apply[C, E](action: C => Option[E]) +final case class Combine[A, B](first: A, second: B) + +given [C, E] => Apply[C, E] is Combinator: + type Context = C + type Element = E + extension(self: Apply[C, E]) + def parse(context: C): Option[E] = self.action(context) + +given [A: Combinator, B: Combinator { type Context = A.Context }] + => Combine[A, B] is Combinator: + type Context = A.Context + type Element = (A.Element, B.Element) + extension(self: Combine[A, B]) + def parse(context: Context): Option[Element] = ??? + +extension [A] (buf: mutable.ListBuffer[A]) def popFirst() = + if buf.isEmpty then None + else try Some(buf.head) finally buf.remove(0) + +@main def hello: Unit = + val source = (0 to 10).toList + val stream = source.to(mutable.ListBuffer) + + val n = Apply[mutable.ListBuffer[Int], Int](s => s.popFirst()) + val m = Combine(n, n) + + val r = m.parse(stream) // error: type mismatch, found `mutable.ListBuffer[Int]`, required `?1.Context` + val rc: Option[(Int, Int)] = r + // it would be great if this worked diff --git a/tests/pos/typeclasses-arrow.scala b/tests/pos/typeclasses-arrow.scala new file mode 100644 index 000000000000..186ae6b6b1b1 --- /dev/null +++ b/tests/pos/typeclasses-arrow.scala @@ -0,0 +1,143 @@ +//> using options -language:experimental.modularity -source future + +// this should go in Predef +infix type is[A <: AnyKind, B <: {type This <: AnyKind}] = B { type This = A } + +class Common: + + trait Ord: + type This + extension (x: This) + def compareTo(y: This): Int + def < (y: This): Boolean = compareTo(y) < 0 + def > (y: This): Boolean = compareTo(y) > 0 + def <= (y: This): Boolean = compareTo(y) <= 0 + def >= (y: This): Boolean = compareTo(y) >= 0 + def max(y: This): This = if x < y then y else x + + trait Show: + type This + extension (x: This) def show: String + + trait SemiGroup: + type This + extension (x: This) def combine(y: This): This + + trait Monoid extends SemiGroup: + def unit: This + + trait Functor: + type This[A] + extension [A](x: This[A]) def map[B](f: A => B): This[B] + + trait Monad extends Functor: + def pure[A](x: A): This[A] + extension [A](x: This[A]) + def flatMap[B](f: A => This[B]): This[B] + def map[B](f: A => B) = x.flatMap(f `andThen` pure) +end Common + +object Instances extends Common: + + given Int is Ord as intOrd: + extension (x: Int) + def compareTo(y: Int) = + if x < y then -1 + else if x > y then +1 + else 0 + + given [T: Ord] => List[T] is Ord: + extension (xs: List[T]) def compareTo(ys: List[T]): Int = (xs, ys) match + case (Nil, Nil) => 0 + case (Nil, _) => -1 + case (_, Nil) => +1 + case (x :: xs1, y :: ys1) => + val fst = x.compareTo(y) + if (fst != 0) fst else xs1.compareTo(ys1) + + given List is Monad as listMonad: + extension [A](xs: List[A]) def flatMap[B](f: A => List[B]): List[B] = + xs.flatMap(f) + def pure[A](x: A): List[A] = + List(x) + + type Reader[Ctx] = [X] =>> Ctx => X + + given [Ctx] => Reader[Ctx] is Monad as readerMonad: + extension [A](r: Ctx => A) def flatMap[B](f: A => Ctx => B): Ctx => B = + ctx => f(r(ctx))(ctx) + def pure[A](x: A): Ctx => A = + ctx => x + + extension (xs: Seq[String]) + def longestStrings: Seq[String] = + val maxLength = xs.map(_.length).max + xs.filter(_.length == maxLength) + + extension [T](xs: List[T]) + def second = xs.tail.head + def third = xs.tail.tail.head + + extension [M[_]: Monad, A](xss: M[M[A]]) + def flatten: M[A] = + xss.flatMap(identity) + + def maximum[T: Ord](xs: List[T]): T = + xs.reduce(_ `max` _) + + given [T: Ord] => T is Ord as descending: + extension (x: T) def compareTo(y: T) = T.compareTo(y)(x) + + def minimum[T: Ord](xs: List[T]) = + maximum(xs)(using descending) + + def test(): Unit = + val xs = List(1, 2, 3) + println(maximum(xs)) + println(maximum(xs)(using descending)) + println(maximum(xs)(using descending(using intOrd))) + println(minimum(xs)) + +// Adapted from the Rust by Example book: https://doc.rust-lang.org/rust-by-example/trait.html +// +// lines words chars +// wc Scala: 28 105 793 +// wc Rust : 57 193 1466 + +trait Animal: + type This + // Associated function signature; `This` refers to the implementor type. + def apply(name: String): This + + // Method signatures; these will return a string. + extension (self: This) + def name: String + def noise: String + def talk(): Unit = println(s"$name, $noise") +end Animal + +class Sheep(val name: String): + var isNaked = false + def shear() = + if isNaked then + println(s"$name is already naked...") + else + println(s"$name gets a haircut!") + isNaked = true + +given Sheep is Animal: + def apply(name: String) = Sheep(name) + extension (self: This) + def name: String = self.name + def noise: String = if self.isNaked then "baaaaah?" else "baaaaah!" + override def talk(): Unit = + println(s"$name pauses briefly... $noise") + +/* + + - In a type pattern, A <: T, A >: T, A: T, A: _ are all allowed and mean + T is a fresh type variable (T can start with a capital letter). + - instance definitions + - `as m` syntax in context bounds and instance definitions + +*/ From 5df5f3ecd6c6ee40020ea7ae304cdd6f1a569da8 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Jan 2024 21:52:41 +0100 Subject: [PATCH 14/63] Rename This to Self as instance type of type classes "This" suggests the meaning "the type of this", which is not the role of Self in type classes. Also Self is used in Swift and Rust, which have a similar typeclass design. # Conflicts: # compiler/src/dotty/tools/dotc/typer/Typer.scala # tests/pos/parsercombinators-arrow.scala --- .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../src/dotty/tools/dotc/typer/Typer.scala | 6 +-- tests/pos/parsercombinators-arrow.scala | 6 +-- tests/pos/parsercombinators-this.scala | 8 +-- tests/pos/typeclass-aggregates.scala | 32 ++++++------ tests/pos/typeclasses-arrow.scala | 48 +++++++++--------- tests/pos/typeclasses-this.scala | 50 +++++++++---------- tests/pos/typeclasses.scala | 46 ++++++++--------- 8 files changed, 99 insertions(+), 98 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 1313eceb01bf..c33d8eba4e4d 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -384,6 +384,7 @@ object StdNames { val RootPackage: N = "RootPackage" val RootClass: N = "RootClass" val Select: N = "Select" + val Self: N = "Self" val Shape: N = "Shape" val StringContext: N = "StringContext" val This: N = "This" diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index f4ea1c587001..0bfe4c7e02b3 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2210,12 +2210,12 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val tparam = untpd.Ident(tree.paramName).withSpan(tree.span) if tycon.tpe.typeParams.nonEmpty then typed(untpd.AppliedTypeTree(tyconSplice, tparam :: Nil)) - else if Feature.enabled(modularity) && tycon.tpe.member(tpnme.This).symbol.isAbstractType then - typed(untpd.RefinedTypeTree(tyconSplice, List(untpd.TypeDef(tpnme.This, tparam)))) + else if Feature.enabled(modularity) && tycon.tpe.member(tpnme.Self).symbol.isAbstractType then + typed(untpd.RefinedTypeTree(tyconSplice, List(untpd.TypeDef(tpnme.Self, tparam)))) else errorTree(tree, em"""Illegal context bound: ${tycon.tpe} does not take type parameters and - |does not have an abstract type member named `This` either.""") + |does not have an abstract type member named `Self` either.""") def typedSingletonTypeTree(tree: untpd.SingletonTypeTree)(using Context): SingletonTypeTree = { val ref1 = typedExpr(tree.ref, SingletonTypeProto) diff --git a/tests/pos/parsercombinators-arrow.scala b/tests/pos/parsercombinators-arrow.scala index b2d85aa1d49c..6d49a0eb5e9b 100644 --- a/tests/pos/parsercombinators-arrow.scala +++ b/tests/pos/parsercombinators-arrow.scala @@ -1,19 +1,19 @@ //> using options -language:experimental.modularity -source future import collection.mutable -infix type is[A <: AnyKind, B <: {type This <: AnyKind}] = B { type This = A } +infix type is[A <: AnyKind, B <: {type Self <: AnyKind}] = B { type Self = A } /// A parser combinator. trait Combinator: - type This + type Self /// The context from which elements are being parsed, typically a stream of tokens. type Context /// The element being parsed. type Element - extension (self: This) + extension (self: Self) /// Parses and returns an element from `context`. def parse(context: Context): Option[Element] end Combinator diff --git a/tests/pos/parsercombinators-this.scala b/tests/pos/parsercombinators-this.scala index 6a38613b4ec6..70b423985400 100644 --- a/tests/pos/parsercombinators-this.scala +++ b/tests/pos/parsercombinators-this.scala @@ -4,14 +4,14 @@ import collection.mutable /// A parser combinator. trait Combinator: - type This + type Self /// The context from which elements are being parsed, typically a stream of tokens. type Context /// The element being parsed. type Element - extension (self: This) + extension (self: Self) /// Parses and returns an element from `context`. def parse(context: Context): Option[Element] end Combinator @@ -20,7 +20,7 @@ final case class Apply[C, E](action: C => Option[E]) final case class Combine[A, B](first: A, second: B) given apply[C, E]: Combinator with { - type This = Apply[C, E] + type Self = Apply[C, E] type Context = C type Element = E extension(self: Apply[C, E]) { @@ -30,7 +30,7 @@ given apply[C, E]: Combinator with { given combine[A: Combinator, B: Combinator { type Context = A.Context }] : Combinator with - type This = Combine[A, B] + type Self = Combine[A, B] type Context = A.Context type Element = (A.Element, B.Element) extension(self: Combine[A, B]) diff --git a/tests/pos/typeclass-aggregates.scala b/tests/pos/typeclass-aggregates.scala index 9bb576603b7b..5e4551b226b7 100644 --- a/tests/pos/typeclass-aggregates.scala +++ b/tests/pos/typeclass-aggregates.scala @@ -1,47 +1,47 @@ //> using options -source future -language:experimental.modularity trait Ord: - type This - extension (x: This) - def compareTo(y: This): Int - def < (y: This): Boolean = compareTo(y) < 0 - def > (y: This): Boolean = compareTo(y) > 0 + type Self + extension (x: Self) + def compareTo(y: Self): Int + def < (y: Self): Boolean = compareTo(y) < 0 + def > (y: Self): Boolean = compareTo(y) > 0 trait OrdProxy extends Ord: export Ord.this.* trait SemiGroup: - type This - extension (x: This) def combine(y: This): This + type Self + extension (x: Self) def combine(y: Self): Self trait SemiGroupProxy extends SemiGroup: export SemiGroup.this.* trait Monoid extends SemiGroup: - def unit: This + def unit: Self trait MonoidProxy extends Monoid: export Monoid.this.* -def ordWithMonoid(ord: Ord, monoid: Monoid{ type This = ord.This }): Ord & Monoid = +def ordWithMonoid(ord: Ord, monoid: Monoid{ type Self = ord.Self }): Ord & Monoid = new ord.OrdProxy with monoid.MonoidProxy {} trait OrdWithMonoid extends Ord, Monoid -def ordWithMonoid2(ord: Ord, monoid: Monoid{ type This = ord.This }) = //: OrdWithMonoid { type This = ord.This} = +def ordWithMonoid2(ord: Ord, monoid: Monoid{ type Self = ord.Self }) = //: OrdWithMonoid { type Self = ord.Self} = new OrdWithMonoid with ord.OrdProxy with monoid.MonoidProxy {} -given intOrd: (Ord { type This = Int }) = ??? -given intMonoid: (Monoid { type This = Int }) = ??? +given intOrd: (Ord { type Self = Int }) = ??? +given intMonoid: (Monoid { type Self = Int }) = ??? -//given (using ord: Ord, monoid: Monoid{ type This = ord.This }): (Ord & Monoid { type This = ord.This}) = +//given (using ord: Ord, monoid: Monoid{ type Self = ord.Self }): (Ord & Monoid { type Self = ord.Self}) = // ordWithMonoid2(ord, monoid) -val x = summon[Ord & Monoid { type This = Int}] -val y: Int = ??? : x.This +val x = summon[Ord & Monoid { type Self = Int}] +val y: Int = ??? : x.Self // given [A, B](using ord: A is Ord, monoid: A is Monoid) => A is Ord & Monoid = // new ord.OrdProxy with monoid.MonoidProxy {} -given [A](using ord: Ord { type This = A }, monoid: Monoid { type This = A}): ((Ord & Monoid) { type This = A}) = +given [A](using ord: Ord { type Self = A }, monoid: Monoid { type Self = A}): ((Ord & Monoid) { type Self = A}) = new ord.OrdProxy with monoid.MonoidProxy {} diff --git a/tests/pos/typeclasses-arrow.scala b/tests/pos/typeclasses-arrow.scala index 186ae6b6b1b1..3b38fb2e3a1d 100644 --- a/tests/pos/typeclasses-arrow.scala +++ b/tests/pos/typeclasses-arrow.scala @@ -1,39 +1,39 @@ //> using options -language:experimental.modularity -source future // this should go in Predef -infix type is[A <: AnyKind, B <: {type This <: AnyKind}] = B { type This = A } +infix type is[A <: AnyKind, B <: {type Self <: AnyKind}] = B { type Self = A } class Common: trait Ord: - type This - extension (x: This) - def compareTo(y: This): Int - def < (y: This): Boolean = compareTo(y) < 0 - def > (y: This): Boolean = compareTo(y) > 0 - def <= (y: This): Boolean = compareTo(y) <= 0 - def >= (y: This): Boolean = compareTo(y) >= 0 - def max(y: This): This = if x < y then y else x + type Self + extension (x: Self) + def compareTo(y: Self): Int + def < (y: Self): Boolean = compareTo(y) < 0 + def > (y: Self): Boolean = compareTo(y) > 0 + def <= (y: Self): Boolean = compareTo(y) <= 0 + def >= (y: Self): Boolean = compareTo(y) >= 0 + def max(y: Self): Self = if x < y then y else x trait Show: - type This - extension (x: This) def show: String + type Self + extension (x: Self) def show: String trait SemiGroup: - type This - extension (x: This) def combine(y: This): This + type Self + extension (x: Self) def combine(y: Self): Self trait Monoid extends SemiGroup: - def unit: This + def unit: Self trait Functor: - type This[A] - extension [A](x: This[A]) def map[B](f: A => B): This[B] + type Self[A] + extension [A](x: Self[A]) def map[B](f: A => B): Self[B] trait Monad extends Functor: - def pure[A](x: A): This[A] - extension [A](x: This[A]) - def flatMap[B](f: A => This[B]): This[B] + def pure[A](x: A): Self[A] + extension [A](x: Self[A]) + def flatMap[B](f: A => Self[B]): Self[B] def map[B](f: A => B) = x.flatMap(f `andThen` pure) end Common @@ -105,12 +105,12 @@ object Instances extends Common: // wc Rust : 57 193 1466 trait Animal: - type This - // Associated function signature; `This` refers to the implementor type. - def apply(name: String): This + type Self + // Associated function signature; `Self` refers to the implementor type. + def apply(name: String): Self // Method signatures; these will return a string. - extension (self: This) + extension (self: Self) def name: String def noise: String def talk(): Unit = println(s"$name, $noise") @@ -127,7 +127,7 @@ class Sheep(val name: String): given Sheep is Animal: def apply(name: String) = Sheep(name) - extension (self: This) + extension (self: Self) def name: String = self.name def noise: String = if self.isNaked then "baaaaah?" else "baaaaah!" override def talk(): Unit = diff --git a/tests/pos/typeclasses-this.scala b/tests/pos/typeclasses-this.scala index 7fb3e553fea7..f0b85b9d9245 100644 --- a/tests/pos/typeclasses-this.scala +++ b/tests/pos/typeclasses-this.scala @@ -1,39 +1,39 @@ //> using options -language:experimental.modularity -source future // this should go in Predef -infix type is[A <: AnyKind, B <: {type This <: AnyKind}] = B { type This = A } +infix type is[A <: AnyKind, B <: {type Self <: AnyKind}] = B { type Self = A } class Common: trait Ord: - type This - extension (x: This) - def compareTo(y: This): Int - def < (y: This): Boolean = compareTo(y) < 0 - def > (y: This): Boolean = compareTo(y) > 0 - def <= (y: This): Boolean = compareTo(y) <= 0 - def >= (y: This): Boolean = compareTo(y) >= 0 - def max(y: This): This = if x < y then y else x + type Self + extension (x: Self) + def compareTo(y: Self): Int + def < (y: Self): Boolean = compareTo(y) < 0 + def > (y: Self): Boolean = compareTo(y) > 0 + def <= (y: Self): Boolean = compareTo(y) <= 0 + def >= (y: Self): Boolean = compareTo(y) >= 0 + def max(y: Self): Self = if x < y then y else x trait Show: - type This - extension (x: This) def show: String + type Self + extension (x: Self) def show: String trait SemiGroup: - type This - extension (x: This) def combine(y: This): This + type Self + extension (x: Self) def combine(y: Self): Self trait Monoid extends SemiGroup: - def unit: This + def unit: Self trait Functor: - type This[A] - extension [A](x: This[A]) def map[B](f: A => B): This[B] + type Self[A] + extension [A](x: Self[A]) def map[B](f: A => B): Self[B] trait Monad extends Functor: - def pure[A](x: A): This[A] - extension [A](x: This[A]) - def flatMap[B](f: A => This[B]): This[B] + def pure[A](x: A): Self[A] + extension [A](x: Self[A]) + def flatMap[B](f: A => Self[B]): Self[B] def map[B](f: A => B) = x.flatMap(f `andThen` pure) end Common @@ -46,7 +46,7 @@ object Instances extends Common: else if x > y then +1 else 0 -// given [T](using tracked val ev: Ord { type This = T}): Ord { type This = List[T] } with +// given [T](using tracked val ev: Ord { type Self = T}): Ord { type Self = List[T] } with given [T: Ord]: List[T] is Ord with extension (xs: List[T]) def compareTo(ys: List[T]): Int = (xs, ys) match case (Nil, Nil) => 0 @@ -106,12 +106,12 @@ object Instances extends Common: // wc Rust : 57 193 1466 trait Animal: - type This - // Associated function signature; `This` refers to the implementor type. - def apply(name: String): This + type Self + // Associated function signature; `Self` refers to the implementor type. + def apply(name: String): Self // Method signatures; these will return a string. - extension (self: This) + extension (self: Self) def name: String def noise: String def talk(): Unit = println(s"$name, $noise") @@ -128,7 +128,7 @@ class Sheep(val name: String): given Sheep is Animal with def apply(name: String) = Sheep(name) - extension (self: This) + extension (self: Self) def name: String = self.name def noise: String = if self.isNaked then "baaaaah?" else "baaaaah!" override def talk(): Unit = diff --git a/tests/pos/typeclasses.scala b/tests/pos/typeclasses.scala index 2bf7f76f0804..7b569407faa2 100644 --- a/tests/pos/typeclasses.scala +++ b/tests/pos/typeclasses.scala @@ -3,30 +3,30 @@ class Common: trait Ord: - type This - extension (x: This) - def compareTo(y: This): Int - def < (y: This): Boolean = compareTo(y) < 0 - def > (y: This): Boolean = compareTo(y) > 0 + type Self + extension (x: Self) + def compareTo(y: Self): Int + def < (y: Self): Boolean = compareTo(y) < 0 + def > (y: Self): Boolean = compareTo(y) > 0 trait SemiGroup: - type This - extension (x: This) def combine(y: This): This + type Self + extension (x: Self) def combine(y: Self): Self trait Monoid extends SemiGroup: - def unit: This + def unit: Self trait Functor: - type This[A] - extension [A](x: This[A]) def map[B](f: A => B): This[B] + type Self[A] + extension [A](x: Self[A]) def map[B](f: A => B): Self[B] trait Monad extends Functor: - def pure[A](x: A): This[A] - extension [A](x: This[A]) - def flatMap[B](f: A => This[B]): This[B] + def pure[A](x: A): Self[A] + extension [A](x: Self[A]) + def flatMap[B](f: A => Self[B]): Self[B] def map[B](f: A => B) = x.flatMap(f `andThen` pure) - infix type is[A <: AnyKind, B <: {type This <: AnyKind}] = B { type This = A } + infix type is[A <: AnyKind, B <: {type Self <: AnyKind}] = B { type Self = A } end Common @@ -34,7 +34,7 @@ end Common object Instances extends Common: given intOrd: (Int is Ord) with - type This = Int + type Self = Int extension (x: Int) def compareTo(y: Int) = if x < y then -1 @@ -77,8 +77,8 @@ object Instances extends Common: def second = xs.tail.head def third = xs.tail.tail.head - extension [M, A](using m: Monad)(xss: m.This[m.This[A]]) - def flatten: m.This[A] = + extension [M, A](using m: Monad)(xss: m.Self[m.Self[A]]) + def flatten: m.Self[A] = xss.flatMap(identity) def maximum[T](xs: List[T])(using T is Ord): T = @@ -103,12 +103,12 @@ object Instances extends Common: // wc Scala: 30 115 853 // wc Rust : 57 193 1466 trait Animal: - type This - // Associated function signature; `This` refers to the implementor type. - def apply(name: String): This + type Self + // Associated function signature; `Self` refers to the implementor type. + def apply(name: String): Self // Method signatures; these will return a string. - extension (self: This) + extension (self: Self) def name: String def noise: String def talk(): Unit = println(s"$name, $noise") @@ -126,7 +126,7 @@ class Sheep(val name: String): /* instance Sheep: Animal with def apply(name: String) = Sheep(name) - extension (self: This) + extension (self: Self) def name: String = self.name def noise: String = if self.isNaked then "baaaaah?" else "baaaaah!" override def talk(): Unit = @@ -137,7 +137,7 @@ import Instances.is // Implement the `Animal` trait for `Sheep`. given (Sheep is Animal) with def apply(name: String) = Sheep(name) - extension (self: This) + extension (self: Self) def name: String = self.name def noise: String = if self.isNaked then "baaaaah?" else "baaaaah!" override def talk(): Unit = From 5482680f2075ebb493f6a0ae4b2391bfedec7b98 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Jan 2024 21:54:45 +0100 Subject: [PATCH 15/63] Add `is` type to Predef --- library/src/scala/runtime/stdLibPatches/Predef.scala | 12 ++++++++++++ tests/pos/parsercombinators-arrow.scala | 2 -- tests/pos/typeclasses-arrow.scala | 3 --- tests/pos/typeclasses-this.scala | 3 --- tests/pos/typeclasses.scala | 3 --- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/library/src/scala/runtime/stdLibPatches/Predef.scala b/library/src/scala/runtime/stdLibPatches/Predef.scala index 09feaf11c31d..95079298f9ce 100644 --- a/library/src/scala/runtime/stdLibPatches/Predef.scala +++ b/library/src/scala/runtime/stdLibPatches/Predef.scala @@ -61,4 +61,16 @@ object Predef: inline def ne(inline y: AnyRef | Null): Boolean = !(x eq y) + /** A type supporting Self-based type classes. + * + * A is TC + * + * expands to + * + * TC { type Self = A } + * + * which is what is needed for a context bound `[A: TC]`. + */ + infix type is[A <: AnyKind, B <: {type Self <: AnyKind}] = B { type Self = A } + end Predef diff --git a/tests/pos/parsercombinators-arrow.scala b/tests/pos/parsercombinators-arrow.scala index 6d49a0eb5e9b..f8bec02067e5 100644 --- a/tests/pos/parsercombinators-arrow.scala +++ b/tests/pos/parsercombinators-arrow.scala @@ -1,8 +1,6 @@ //> using options -language:experimental.modularity -source future import collection.mutable -infix type is[A <: AnyKind, B <: {type Self <: AnyKind}] = B { type Self = A } - /// A parser combinator. trait Combinator: diff --git a/tests/pos/typeclasses-arrow.scala b/tests/pos/typeclasses-arrow.scala index 3b38fb2e3a1d..379365ffa1c5 100644 --- a/tests/pos/typeclasses-arrow.scala +++ b/tests/pos/typeclasses-arrow.scala @@ -1,8 +1,5 @@ //> using options -language:experimental.modularity -source future -// this should go in Predef -infix type is[A <: AnyKind, B <: {type Self <: AnyKind}] = B { type Self = A } - class Common: trait Ord: diff --git a/tests/pos/typeclasses-this.scala b/tests/pos/typeclasses-this.scala index f0b85b9d9245..20ce78678b22 100644 --- a/tests/pos/typeclasses-this.scala +++ b/tests/pos/typeclasses-this.scala @@ -1,8 +1,5 @@ //> using options -language:experimental.modularity -source future -// this should go in Predef -infix type is[A <: AnyKind, B <: {type Self <: AnyKind}] = B { type Self = A } - class Common: trait Ord: diff --git a/tests/pos/typeclasses.scala b/tests/pos/typeclasses.scala index 7b569407faa2..d0315a318310 100644 --- a/tests/pos/typeclasses.scala +++ b/tests/pos/typeclasses.scala @@ -26,8 +26,6 @@ class Common: def flatMap[B](f: A => Self[B]): Self[B] def map[B](f: A => B) = x.flatMap(f `andThen` pure) - infix type is[A <: AnyKind, B <: {type Self <: AnyKind}] = B { type Self = A } - end Common @@ -132,7 +130,6 @@ instance Sheep: Animal with override def talk(): Unit = println(s"$name pauses briefly... $noise") */ -import Instances.is // Implement the `Animal` trait for `Sheep`. given (Sheep is Animal) with From 89e91ae94827db71b79bb0f79af37ca9d953107f Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Jan 2024 22:05:05 +0100 Subject: [PATCH 16/63] Allow multiple context bounds in `{...}` --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 11 ++++++++--- tests/pos/FromString.scala | 13 +++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 tests/pos/FromString.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 6246b468d3eb..9af5e5dae18f 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2167,11 +2167,16 @@ object Parsers { else atSpan((t.span union cbs.head.span).start) { ContextBounds(t, cbs) } } + /** ContextBound ::= Type [`as` id] */ + def contextBound(pname: TypeName): Tree = + ContextBoundTypeTree(toplevelTyp(), pname) + def contextBounds(pname: TypeName): List[Tree] = if in.isColon then - atSpan(in.skipToken()) { - ContextBoundTypeTree(toplevelTyp(), pname) - } :: contextBounds(pname) + in.nextToken() + if in.token == LBRACE && in.featureEnabled(Feature.modularity) + then inBraces(commaSeparated(() => contextBound(pname))) + else contextBound(pname) :: contextBounds(pname) else if in.token == VIEWBOUND then report.errorOrMigrationWarning( em"view bounds `<%' are no longer supported, use a context bound `:' instead", diff --git a/tests/pos/FromString.scala b/tests/pos/FromString.scala new file mode 100644 index 000000000000..b73da23ae85e --- /dev/null +++ b/tests/pos/FromString.scala @@ -0,0 +1,13 @@ +//> using options -language:experimental.modularity -source future + +trait FromString: + type Self + def fromString(s: String): Self + +given Int is FromString = _.toInt + +given Double is FromString = _.toDouble + +def add[N: {FromString, Numeric}](a: String, b: String): N = + val num = summon[Numeric[N]] + num.plus(N.fromString(a), N.fromString(b)) From cf59585cb9426ace7eb09d72301cc51677e0790c Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Jan 2024 22:20:22 +0100 Subject: [PATCH 17/63] Allow renamings `as N` in context bounds --- .../src/dotty/tools/dotc/ast/Desugar.scala | 15 +++++++------ compiler/src/dotty/tools/dotc/ast/untpd.scala | 14 ++++++------- .../dotty/tools/dotc/parsing/Parsers.scala | 16 +++++++++++--- .../tools/dotc/printing/RefinedPrinter.scala | 21 +++++++++++++------ docs/_docs/internals/syntax.md | 4 +++- tests/pos/FromString.scala | 3 +-- 6 files changed, 48 insertions(+), 25 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 16b11a3c6f68..6dc31a2fdfc7 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -238,16 +238,19 @@ object desugar { var useParamName = Feature.enabled(Feature.modularity) for bound <- cxbounds do val paramName = - if useParamName then - useParamName = false - tparam.name.toTermName - else - seenContextBounds += 1 // Start at 1 like FreshNameCreator. - ContextBoundParamName(EmptyTermName, seenContextBounds) + bound match + case ContextBoundTypeTree(_, _, ownName) if !ownName.isEmpty => + ownName + case _ if useParamName => + tparam.name.toTermName + case _ => + seenContextBounds += 1 // Start at 1 like FreshNameCreator. + ContextBoundParamName(EmptyTermName, seenContextBounds) // Just like with `makeSyntheticParameter` on nameless parameters of // using clauses, we only need names that are unique among the // parameters of the method since shadowing does not affect // implicit resolution in Scala 3. + useParamName = false val evidenceParam = ValDef(paramName, bound, EmptyTree).withFlags(flags) evidenceParam.pushAttachment(ContextBoundParam, ()) evidenceParamBuf += evidenceParam diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 223d9d01e942..9abb818cb567 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -118,7 +118,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case class ContextBounds(bounds: TypeBoundsTree, cxBounds: List[Tree])(implicit @constructorOnly src: SourceFile) extends TypTree case class PatDef(mods: Modifiers, pats: List[Tree], tpt: Tree, rhs: Tree)(implicit @constructorOnly src: SourceFile) extends DefTree case class ExtMethods(paramss: List[ParamClause], methods: List[Tree])(implicit @constructorOnly src: SourceFile) extends Tree - case class ContextBoundTypeTree(tycon: Tree, paramName: TypeName)(implicit @constructorOnly src: SourceFile) extends Tree + case class ContextBoundTypeTree(tycon: Tree, paramName: TypeName, ownName: TermName)(implicit @constructorOnly src: SourceFile) extends Tree case class MacroTree(expr: Tree)(implicit @constructorOnly src: SourceFile) extends Tree case class ImportSelector(imported: Ident, renamed: Tree = EmptyTree, bound: Tree = EmptyTree)(implicit @constructorOnly src: SourceFile) extends Tree { @@ -678,9 +678,9 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { def ExtMethods(tree: Tree)(paramss: List[ParamClause], methods: List[Tree])(using Context): Tree = tree match case tree: ExtMethods if (paramss eq tree.paramss) && (methods == tree.methods) => tree case _ => finalize(tree, untpd.ExtMethods(paramss, methods)(tree.source)) - def ContextBoundTypeTree(tree: Tree)(tycon: Tree, paramName: TypeName)(using Context): Tree = tree match - case tree: ContextBoundTypeTree if (tycon eq tree.tycon) && paramName == tree.paramName => tree - case _ => finalize(tree, untpd.ContextBoundTypeTree(tycon, paramName)(tree.source)) + def ContextBoundTypeTree(tree: Tree)(tycon: Tree, paramName: TypeName, ownName: TermName)(using Context): Tree = tree match + case tree: ContextBoundTypeTree if (tycon eq tree.tycon) && paramName == tree.paramName && ownName == tree.ownName => tree + case _ => finalize(tree, untpd.ContextBoundTypeTree(tycon, paramName, ownName)(tree.source)) def ImportSelector(tree: Tree)(imported: Ident, renamed: Tree, bound: Tree)(using Context): Tree = tree match { case tree: ImportSelector if (imported eq tree.imported) && (renamed eq tree.renamed) && (bound eq tree.bound) => tree case _ => finalize(tree, untpd.ImportSelector(imported, renamed, bound)(tree.source)) @@ -746,8 +746,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { cpy.PatDef(tree)(mods, transform(pats), transform(tpt), transform(rhs)) case ExtMethods(paramss, methods) => cpy.ExtMethods(tree)(transformParamss(paramss), transformSub(methods)) - case ContextBoundTypeTree(tycon, paramName) => - cpy.ContextBoundTypeTree(tree)(transform(tycon), paramName) + case ContextBoundTypeTree(tycon, paramName, ownName) => + cpy.ContextBoundTypeTree(tree)(transform(tycon), paramName, ownName) case ImportSelector(imported, renamed, bound) => cpy.ImportSelector(tree)(transformSub(imported), transform(renamed), transform(bound)) case Number(_, _) | TypedSplice(_) => @@ -803,7 +803,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { this(this(this(x, pats), tpt), rhs) case ExtMethods(paramss, methods) => this(paramss.foldLeft(x)(apply), methods) - case ContextBoundTypeTree(tycon, paramName) => + case ContextBoundTypeTree(tycon, paramName, ownName) => this(x, tycon) case ImportSelector(imported, renamed, bound) => this(this(this(x, imported), renamed), bound) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 9af5e5dae18f..9bc996ede196 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1786,7 +1786,9 @@ object Parsers { def infixTypeRest(t: Tree, operand: Location => Tree = refinedTypeFn): Tree = infixOps(t, canStartInfixTypeTokens, operand, Location.ElseWhere, ParseKind.Type, - isOperator = !followingIsVararg() && !isPureArrow && !isIdent(nme.as) + isOperator = !followingIsVararg() + && !isPureArrow + && !(isIdent(nme.as) && in.featureEnabled(Feature.modularity)) && nextCanFollowOperator(canStartInfixTypeTokens)) /** RefinedType ::= WithType {[nl] Refinement} [`^` CaptureSet] @@ -2158,7 +2160,7 @@ object Parsers { if (in.token == tok) { in.nextToken(); toplevelTyp() } else EmptyTree - /** TypeParamBounds ::= TypeBounds {`<%' Type} {`:' Type} + /** TypeParamBounds ::= TypeBounds {`<%' Type} [`:` ContextBounds] */ def typeParamBounds(pname: TypeName): Tree = { val t = typeBounds() @@ -2169,8 +2171,16 @@ object Parsers { /** ContextBound ::= Type [`as` id] */ def contextBound(pname: TypeName): Tree = - ContextBoundTypeTree(toplevelTyp(), pname) + val t = toplevelTyp() + val ownName = + if isIdent(nme.as) && in.featureEnabled(Feature.modularity) then + in.nextToken() + ident() + else EmptyTermName + ContextBoundTypeTree(t, pname, ownName) + /** ContextBounds ::= ContextBound | `{` ContextBound {`,` ContextBound} `}` + */ def contextBounds(pname: TypeName): List[Tree] = if in.isColon then in.nextToken() diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 8efd0b21005a..a593bc6fdda5 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -373,7 +373,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { changePrec(GlobalPrec) { keywordStr("for ") ~ Text(enums map enumText, "; ") ~ sep ~ toText(expr) } def cxBoundToText(bound: untpd.Tree): Text = bound match { // DD - case ContextBoundTypeTree(tpt, _) => " : " ~ toText(tpt) + case ContextBoundTypeTree(tpt, _, _) => " : " ~ toText(tpt) case untpd.Function(_, tpt) => " <% " ~ toText(tpt) } @@ -729,9 +729,18 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { case GenAlias(pat, expr) => toText(pat) ~ " = " ~ toText(expr) case ContextBounds(bounds, cxBounds) => - cxBounds.foldLeft(toText(bounds)) {(t, cxb) => - t ~ cxBoundToText(cxb) - } + if Feature.enabled(Feature.modularity) then + def boundsText(bounds: Tree) = bounds match + case ContextBoundTypeTree(tpt, _, ownName) => + toText(tpt) ~ (" as " ~ toText(ownName) `provided` !ownName.isEmpty) + case bounds => toText(bounds) + cxBounds match + case bound :: Nil => ": " ~ boundsText(bound) + case _ => ": {" ~ Text(cxBounds.map(boundsText), ", ") ~ "}" + else + cxBounds.foldLeft(toText(bounds)) {(t, cxb) => + t ~ cxBoundToText(cxb) + } case PatDef(mods, pats, tpt, rhs) => modText(mods, NoSymbol, keywordStr("val"), isType = false) ~~ toText(pats, ", ") ~ optAscription(tpt) ~ optText(rhs)(" = " ~ _) @@ -776,8 +785,8 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { prefix ~~ idx.toString ~~ "|" ~~ tpeText ~~ "|" ~~ argsText ~~ "|" ~~ contentText ~~ postfix case CapturesAndResult(refs, parent) => changePrec(GlobalPrec)("^{" ~ Text(refs.map(toText), ", ") ~ "}" ~ toText(parent)) - case ContextBoundTypeTree(tycon, pname) => - toText(pname) ~ " is " ~ toText(tycon) + case ContextBoundTypeTree(tycon, pname, ownName) => + toText(pname) ~ " is " ~ toText(tycon) ~ (" as " ~ toText(ownName) `provided` !ownName.isEmpty) case _ => tree.fallbackToText(this) } diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index 0ef7de343278..37b6110a6df7 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -221,7 +221,9 @@ IntoTargetType ::= Type TypeArgs ::= ‘[’ Types ‘]’ ts Refinement ::= :<<< [RefineDcl] {semi [RefineDcl]} >>> ds TypeBounds ::= [‘>:’ Type] [‘<:’ Type] TypeBoundsTree(lo, hi) -TypeParamBounds ::= TypeBounds {‘:’ Type} ContextBounds(typeBounds, tps) +TypeParamBounds ::= TypeBounds [‘:’ ContextBounds] ContextBounds(typeBounds, tps) +ContextBounds ::= ContextBound | '{' ContextBound {',' ContextBound} '}' +ContextBound ::= Type ['as' id] Types ::= Type {‘,’ Type} ``` diff --git a/tests/pos/FromString.scala b/tests/pos/FromString.scala index b73da23ae85e..4a99f3df3abc 100644 --- a/tests/pos/FromString.scala +++ b/tests/pos/FromString.scala @@ -8,6 +8,5 @@ given Int is FromString = _.toInt given Double is FromString = _.toDouble -def add[N: {FromString, Numeric}](a: String, b: String): N = - val num = summon[Numeric[N]] +def add[N: {FromString, Numeric as num}](a: String, b: String): N = num.plus(N.fromString(a), N.fromString(b)) From 73deadcd455d1a82ff66a8e5f0fb6d84138123ac Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Jan 2024 22:41:48 +0100 Subject: [PATCH 18/63] Implement `deferredSummon` A definition like `given T = deferredSummon` in a trait will be expanded to an abstract given in the trait that is implemented automatically in all classes inheriting the trait. --- .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../src/dotty/tools/dotc/core/Symbols.scala | 5 +- .../src/dotty/tools/dotc/typer/Namer.scala | 12 + .../src/dotty/tools/dotc/typer/Typer.scala | 45 ++- tests/neg/deferredSummon.scala | 12 + tests/pos/deferredSummon.scala | 29 ++ tests/pos/hylolib-extract.scala | 18 + tests/pos/hylolib/AnyCollection.scala | 69 ++++ tests/pos/hylolib/AnyValue.scala | 76 ++++ tests/pos/hylolib/BitArray.scala | 375 ++++++++++++++++++ tests/pos/hylolib/Collection.scala | 281 +++++++++++++ tests/pos/hylolib/CoreTraits.scala | 57 +++ tests/pos/hylolib/Hasher.scala | 38 ++ tests/pos/hylolib/HyArray.scala | 224 +++++++++++ tests/pos/hylolib/Integers.scala | 58 +++ tests/pos/hylolib/Range.scala | 37 ++ tests/pos/hylolib/Slice.scala | 49 +++ tests/pos/hylolib/StringConvertible.scala | 14 + 18 files changed, 1391 insertions(+), 9 deletions(-) create mode 100644 tests/neg/deferredSummon.scala create mode 100644 tests/pos/deferredSummon.scala create mode 100644 tests/pos/hylolib-extract.scala create mode 100644 tests/pos/hylolib/AnyCollection.scala create mode 100644 tests/pos/hylolib/AnyValue.scala create mode 100644 tests/pos/hylolib/BitArray.scala create mode 100644 tests/pos/hylolib/Collection.scala create mode 100644 tests/pos/hylolib/CoreTraits.scala create mode 100644 tests/pos/hylolib/Hasher.scala create mode 100644 tests/pos/hylolib/HyArray.scala create mode 100644 tests/pos/hylolib/Integers.scala create mode 100644 tests/pos/hylolib/Range.scala create mode 100644 tests/pos/hylolib/Slice.scala create mode 100644 tests/pos/hylolib/StringConvertible.scala diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index c33d8eba4e4d..42103922b2ec 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -453,6 +453,7 @@ object StdNames { val create: N = "create" val currentMirror: N = "currentMirror" val curried: N = "curried" + val deferredSummon: N = "deferredSummon" val definitions: N = "definitions" val delayedInit: N = "delayedInit" val delayedInitArg: N = "delayedInit$body" diff --git a/compiler/src/dotty/tools/dotc/core/Symbols.scala b/compiler/src/dotty/tools/dotc/core/Symbols.scala index 096e9c30dd70..49fc53c172df 100644 --- a/compiler/src/dotty/tools/dotc/core/Symbols.scala +++ b/compiler/src/dotty/tools/dotc/core/Symbols.scala @@ -397,13 +397,12 @@ object Symbols extends SymUtils { flags: FlagSet = this.flags, info: Type = this.info, privateWithin: Symbol = this.privateWithin, - coord: Coord = NoCoord, // Can be `= owner.coord` once we bootstrap - compUnitInfo: CompilationUnitInfo | Null = null // Can be `= owner.associatedFile` once we bootstrap + coord: Coord = NoCoord, // Can be `= owner.coord` once we have new default args + compUnitInfo: CompilationUnitInfo | Null = null // Can be `= owner.compilationUnitInfo` once we have new default args ): Symbol = { val coord1 = if (coord == NoCoord) owner.coord else coord val compilationUnitInfo1 = if (compilationUnitInfo == null) owner.compilationUnitInfo else compilationUnitInfo - if isClass then newClassSymbol(owner, name.asTypeName, flags, _ => info, privateWithin, coord1, compilationUnitInfo1) else diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 14b66e0c374a..8115cf7483c2 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -1805,6 +1805,18 @@ class Namer { typer: Typer => case _ => WildcardType } + + // translate `given T = deferredSummon` to an abstract given with HasDefault flag + if sym.is(Given, butNot = Method) then + mdef.rhs match + case Ident(nme.deferredSummon) if Feature.enabled(modularity) => + if !sym.maybeOwner.is(Trait) then + report.error(em"`deferredSummon` can only be used for givens in traits", mdef.rhs.srcPos) + else + sym.resetFlag(Final | Lazy) + sym.setFlag(Deferred | HasDefault) + case _ => + val mbrTpe = paramFn(checkSimpleKinded(typedAheadType(mdef.tpt, tptProto)).tpe) if (ctx.explicitNulls && mdef.mods.is(JavaDefined)) JavaNullInterop.nullifyMember(sym, mbrTpe, mdef.mods.isAllOf(JavaEnumValue)) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 0bfe4c7e02b3..ad86535f4eb8 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2572,12 +2572,16 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val ValDef(name, tpt, _) = vdef checkNonRootName(vdef.name, vdef.nameSpan) completeAnnotations(vdef, sym) - if (sym.isOneOf(GivenOrImplicit)) checkImplicitConversionDefOK(sym) + if sym.is(Implicit) then checkImplicitConversionDefOK(sym) if sym.is(Module) then checkNoModuleClash(sym) val tpt1 = checkSimpleKinded(typedType(tpt)) val rhs1 = vdef.rhs match { - case rhs @ Ident(nme.WILDCARD) => rhs withType tpt1.tpe - case rhs => typedExpr(rhs, tpt1.tpe.widenExpr) + case rhs @ Ident(nme.WILDCARD) => + rhs.withType(tpt1.tpe) + case Ident(nme.deferredSummon) if sym.isAllOf(Given | Deferred | HasDefault, butNot = Param) => + EmptyTree + case rhs => + typedExpr(rhs, tpt1.tpe.widenExpr) } val vdef1 = assignType(cpy.ValDef(vdef)(name, tpt1, rhs1), sym) postProcessInfo(vdef1, sym) @@ -2821,6 +2825,34 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case None => body + /** Implement givens that were declared with a `deferredSummon` rhs. + * The a given value matching the declared type is searched in a + * context directly enclosing the current class, in which all given + * parameters of the current class are also defined. + */ + def implementDeferredGivens(body: List[Tree]): List[Tree] = + if cls.is(Trait) then body + else + def givenImpl(mbr: TermRef): ValDef = + val dcl = mbr.symbol + val target = dcl.info.asSeenFrom(cls.thisType, dcl.owner) + val constr = cls.primaryConstructor + val paramScope = newScopeWith(cls.paramAccessors.filter(_.is(Given))*) + val searchCtx = ctx.outer.fresh.setScope(paramScope) + val rhs = implicitArgTree(target, cdef.span)(using searchCtx) + val impl = dcl.copy(cls, + flags = dcl.flags &~ (HasDefault | Deferred) | Final, + info = target, + coord = rhs.span).entered.asTerm + ValDef(impl, rhs) + + val givenImpls = + cls.thisType.implicitMembers + .filter(_.symbol.isAllOf(Given | Deferred | HasDefault, butNot = Param)) + .map(givenImpl) + body ++ givenImpls + end implementDeferredGivens + ensureCorrectSuperClass() completeAnnotations(cdef, cls) val constr1 = typed(constr).asInstanceOf[DefDef] @@ -2842,9 +2874,10 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer else { val dummy = localDummy(cls, impl) val body1 = - addParentRefinements( - addAccessorDefs(cls, - typedStats(impl.body, dummy)(using ctx.inClassContext(self1.symbol))._1)) + implementDeferredGivens( + addParentRefinements( + addAccessorDefs(cls, + typedStats(impl.body, dummy)(using ctx.inClassContext(self1.symbol))._1))) checkNoDoubleDeclaration(cls) val impl1 = cpy.Template(impl)(constr1, parents1, Nil, self1, body1) diff --git a/tests/neg/deferredSummon.scala b/tests/neg/deferredSummon.scala new file mode 100644 index 000000000000..c4c9e36fce34 --- /dev/null +++ b/tests/neg/deferredSummon.scala @@ -0,0 +1,12 @@ +//> using options -language:experimental.modularity + +object Test: + given Int = deferredSummon // error + +abstract class C: + given Int = deferredSummon // error + +trait A: + locally: + given Int = deferredSummon // error + diff --git a/tests/pos/deferredSummon.scala b/tests/pos/deferredSummon.scala new file mode 100644 index 000000000000..34455a46e137 --- /dev/null +++ b/tests/pos/deferredSummon.scala @@ -0,0 +1,29 @@ +//> using options -language:experimental.modularity -source future +trait Ord: + type Self + def less(x: Self, y: Self): Boolean + +trait A: + type Elem + given Elem is Ord = deferredSummon + def foo = summon[Elem is Ord] + +object Inst: + given Int is Ord: + def less(x: Int, y: Int) = x < y + +object Test: + import Inst.given + class C extends A: + type Elem = Int + object E extends A: + type Elem = Int + given A: + type Elem = Int + +class D[T: Ord] extends A: + type Elem = T + + + + diff --git a/tests/pos/hylolib-extract.scala b/tests/pos/hylolib-extract.scala new file mode 100644 index 000000000000..2cb171bc99fd --- /dev/null +++ b/tests/pos/hylolib-extract.scala @@ -0,0 +1,18 @@ +//> using options -language:experimental.modularity -source future +package hylotest + +trait Value[Self] + +/** A collection of elements accessible by their position. */ +trait Collection[Self]: + + /** The type of the elements in the collection. */ + type Element + given elementIsValue: Value[Element] = deferredSummon + +class BitArray + +given Value[Boolean] {} + +given Collection[BitArray] with + type Element = Boolean diff --git a/tests/pos/hylolib/AnyCollection.scala b/tests/pos/hylolib/AnyCollection.scala new file mode 100644 index 000000000000..55e453d6dc87 --- /dev/null +++ b/tests/pos/hylolib/AnyCollection.scala @@ -0,0 +1,69 @@ +package hylo + +/** A type-erased collection. + * + * A `AnyCollection` forwards its operations to a wrapped value, hiding its implementation. + */ +final class AnyCollection[Element] private ( + val _start: () => AnyValue, + val _end: () => AnyValue, + val _after: (AnyValue) => AnyValue, + val _at: (AnyValue) => Element +) + +object AnyCollection { + + /** Creates an instance forwarding its operations to `base`. */ + def apply[Base](using b: Collection[Base])(base: Base): AnyCollection[b.Element] = + // NOTE: This evidence is redefined so the compiler won't report ambiguity between `intIsValue` + // and `anyValueIsValue` when the method is called on a collection of `Int`s. None of these + // choices is even correct! Note also that the ambiguity is suppressed if the constructor of + // `AnyValue` is declared with a context bound rather than an implicit parameter. + given Value[b.Position] = b.positionIsValue + + def start(): AnyValue = + AnyValue(base.startPosition) + + def end(): AnyValue = + AnyValue(base.endPosition) + + def after(p: AnyValue): AnyValue = + AnyValue(base.positionAfter(p.unsafelyUnwrappedAs[b.Position])) + + def at(p: AnyValue): b.Element = + base.at(p.unsafelyUnwrappedAs[b.Position]) + + new AnyCollection[b.Element]( + _start = start, + _end = end, + _after = after, + _at = at + ) + +} + +given anyCollectionIsCollection[T](using tIsValue: Value[T]): Collection[AnyCollection[T]] with { + + type Element = T + //given elementIsValue: Value[Element] = tIsValue + + type Position = AnyValue + given positionIsValue: Value[Position] = anyValueIsValue + + extension (self: AnyCollection[T]) { + + def startPosition = + self._start() + + def endPosition = + self._end() + + def positionAfter(p: Position) = + self._after(p) + + def at(p: Position) = + self._at(p) + + } + +} diff --git a/tests/pos/hylolib/AnyValue.scala b/tests/pos/hylolib/AnyValue.scala new file mode 100644 index 000000000000..b9d39869c09a --- /dev/null +++ b/tests/pos/hylolib/AnyValue.scala @@ -0,0 +1,76 @@ +package hylo + +/** A wrapper around an object providing a reference API. */ +private final class Ref[T](val value: T) { + + override def toString: String = + s"Ref($value)" + +} + +/** A type-erased value. + * + * An `AnyValue` forwards its operations to a wrapped value, hiding its implementation. + */ +final class AnyValue private ( + private val wrapped: AnyRef, + private val _copy: (AnyRef) => AnyValue, + private val _eq: (AnyRef, AnyRef) => Boolean, + private val _hashInto: (AnyRef, Hasher) => Hasher +) { + + /** Returns a copy of `this`. */ + def copy(): AnyValue = + _copy(this.wrapped) + + /** Returns `true` iff `this` and `other` have an equivalent value. */ + def eq(other: AnyValue): Boolean = + _eq(this.wrapped, other.wrapped) + + /** Hashes the salient parts of `this` into `hasher`. */ + def hashInto(hasher: Hasher): Hasher = + _hashInto(this.wrapped, hasher) + + /** Returns the value wrapped in `this` as an instance of `T`. */ + def unsafelyUnwrappedAs[T]: T = + wrapped.asInstanceOf[Ref[T]].value + + /** Returns a textual description of `this`. */ + override def toString: String = + wrapped.toString + +} + +object AnyValue { + + /** Creates an instance wrapping `wrapped`. */ + def apply[T](using Value[T])(wrapped: T): AnyValue = + def copy(a: AnyRef): AnyValue = + AnyValue(a.asInstanceOf[Ref[T]].value.copy()) + + def eq(a: AnyRef, b: AnyRef): Boolean = + a.asInstanceOf[Ref[T]].value `eq` b.asInstanceOf[Ref[T]].value + + def hashInto(a: AnyRef, hasher: Hasher): Hasher = + a.asInstanceOf[Ref[T]].value.hashInto(hasher) + + new AnyValue(Ref(wrapped), copy, eq, hashInto) + +} + +given anyValueIsValue: Value[AnyValue] with { + + extension (self: AnyValue) { + + def copy(): AnyValue = + self.copy() + + def eq(other: AnyValue): Boolean = + self `eq` other + + def hashInto(hasher: Hasher): Hasher = + self.hashInto(hasher) + + } + +} diff --git a/tests/pos/hylolib/BitArray.scala b/tests/pos/hylolib/BitArray.scala new file mode 100644 index 000000000000..485f30472847 --- /dev/null +++ b/tests/pos/hylolib/BitArray.scala @@ -0,0 +1,375 @@ +package hylo + +import scala.collection.mutable + +/** An array of bit values represented as Booleans, where `true` indicates that the bit is on. */ +final class BitArray private ( + private var _bits: HyArray[Int], + private var _count: Int +) { + + /** Returns `true` iff `this` is empty. */ + def isEmpty: Boolean = + _count == 0 + + /** Returns the number of elements in `this`. */ + def count: Int = + _count + + /** The number of bits that the array can contain before allocating new storage. */ + def capacity: Int = + _bits.capacity << 5 + + /** Reserves enough storage to store `n` elements in `this`. */ + def reserveCapacity(n: Int, assumeUniqueness: Boolean = false): BitArray = + if (n == 0) { + this + } else { + val k = 1 + ((n - 1) >> 5) + if (assumeUniqueness) { + _bits = _bits.reserveCapacity(k, assumeUniqueness) + this + } else { + new BitArray(_bits.reserveCapacity(k), _count) + } + } + + /** Adds a new element at the end of the array. */ + def append(bit: Boolean, assumeUniqueness: Boolean = false): BitArray = + val result = if assumeUniqueness && (count < capacity) then this else copy(count + 1) + val p = BitArray.Position(count) + if (p.bucket >= _bits.count) { + result._bits = _bits.append(if bit then 1 else 0) + } else { + result.setValue(bit, p) + } + result._count += 1 + result + + /** Removes and returns the last element, or returns `None` if the array is empty. */ + def popLast(assumeUniqueness: Boolean = false): (BitArray, Option[Boolean]) = + if (isEmpty) { + (this, None) + } else { + val result = if assumeUniqueness then this else copy() + val bit = result.at(BitArray.Position(count)) + result._count -= 1 + (result, Some(bit)) + } + + /** Removes all elements in the array, keeping allocated storage iff `keepStorage` is true. */ + def removeAll( + keepStorage: Boolean = false, + assumeUniqueness: Boolean = false + ): BitArray = + if (isEmpty) { + this + } else if (keepStorage) { + val result = if assumeUniqueness then this else copy() + result._bits.removeAll(keepStorage, assumeUniqueness = true) + result._count = 0 + result + } else { + BitArray() + } + + /** Returns `true` iff all elements in `this` are `false`. */ + def allFalse: Boolean = + if (isEmpty) { + true + } else { + val k = (count - 1) >> 5 + def loop(i: Int): Boolean = + if (i == k) { + val m = (1 << (count & 31)) - 1 + (_bits.at(k) & m) == 0 + } else if (_bits.at(i) != 0) { + false + } else { + loop(i + 1) + } + loop(0) + } + + /** Returns `true` iff all elements in `this` are `true`. */ + def allTrue: Boolean = + if (isEmpty) { + true + } else { + val k = (count - 1) >> 5 + def loop(i: Int): Boolean = + if (i == k) { + val m = (1 << (count & 31)) - 1 + (_bits.at(k) & m) == m + } else if (_bits.at(i) != ~0) { + false + } else { + loop(i + 1) + } + loop(0) + } + + /** Returns the bitwise OR of `this` and `other`. */ + def | (other: BitArray): BitArray = + val result = copy() + result.applyBitwise(other, _ | _, assumeUniqueness = true) + + /** Returns the bitwise AND of `this` and `other`. */ + def & (other: BitArray): BitArray = + val result = copy() + result.applyBitwise(other, _ & _, assumeUniqueness = true) + + /** Returns the bitwise XOR of `this` and `other`. */ + def ^ (other: BitArray): BitArray = + val result = copy() + result.applyBitwise(other, _ ^ _, assumeUniqueness = true) + + /** Assigns each bits in `this` to the result of `operation` applied on those bits and their + * corresponding bits in `other`. + * + * @requires + * `self.count == other.count`. + */ + private def applyBitwise( + other: BitArray, + operation: (Int, Int) => Int, + assumeUniqueness: Boolean = false + ): BitArray = + require(this.count == other.count) + if (isEmpty) { + this + } else { + val result = if assumeUniqueness then this else copy() + var u = assumeUniqueness + val k = (count - 1) >> 5 + + for (i <- 0 until k) { + result._bits = result._bits.modifyAt( + i, (n) => operation(n, other._bits.at(n)), + assumeUniqueness = u + ) + u = true + } + val m = (1 << (count & 31)) - 1 + result._bits = result._bits.modifyAt( + k, (n) => operation(n & m, other._bits.at(k) & m), + assumeUniqueness = u + ) + + result + } + + /** Returns the position of `this`'s first element', or `endPosition` if `this` is empty. + * + * @complexity + * O(1). + */ + def startPosition: BitArray.Position = + BitArray.Position(0) + + /** Returns the "past the end" position in `this`, that is, the position immediately after the + * last element in `this`. + * + * @complexity + * O(1). + */ + def endPosition: BitArray.Position = + BitArray.Position(count) + + /** Returns the position immediately after `p`. + * + * @requires + * `p` is a valid position in `self` different from `endPosition`. + * @complexity + * O(1). + */ + def positionAfter(p: BitArray.Position): BitArray.Position = + if (p.offsetInBucket == 63) { + BitArray.Position(p.bucket + 1, 0) + } else { + BitArray.Position(p.bucket, p.offsetInBucket + 1) + } + + /** Accesses the element at `p`. + * + * @requires + * `p` is a valid position in `self` different from `endPosition`. + * @complexity + * O(1). + */ + def at(p: BitArray.Position): Boolean = + val m = 1 << p.offsetInBucket + val b: Int = _bits.at(p.bucket) + (b & m) == m + + /** Accesses the `i`-th element of `this`. + * + * @requires + * `i` is greater than or equal to 0, and less than `count`. + * @complexity + * O(1). + */ + def atIndex(i: Int): Boolean = + at(BitArray.Position(i)) + + /** Calls `transform` on the element at `p` to update its value. + * + * @requires + * `p` is a valid position in `self` different from `endPosition`. + * @complexity + * O(1). + */ + def modifyAt( + p: BitArray.Position, + transform: (Boolean) => Boolean, + assumeUniqueness: Boolean = false + ): BitArray = + val result = if assumeUniqueness then this else copy() + result.setValue(transform(result.at(p)), p) + result + + /** Calls `transform` on `i`-th element of `this` to update its value. + * + * @requires + * `i` is greater than or equal to 0, and less than `count`. + * @complexity + * O(1). + */ + def modifyAtIndex( + i: Int, + transform: (Boolean) => Boolean, + assumeUniqueness: Boolean = false + ): BitArray = + modifyAt(BitArray.Position(i), transform, assumeUniqueness) + + /** Returns an independent copy of `this`. */ + def copy(minimumCapacity: Int = 0): BitArray = + if (minimumCapacity > capacity) { + // If the requested capacity on the copy is greater than what we have, `reserveCapacity` will + // create an independent value. + reserveCapacity(minimumCapacity) + } else { + val k = 1 + ((minimumCapacity - 1) >> 5) + val newBits = _bits.copy(k) + new BitArray(newBits, _count) + } + + /** Returns a textual description of `this`. */ + override def toString: String = + _bits.toString + + /** Sets the value `b` for the bit at position `p`. + * + * @requires + * `this` is uniquely referenced and `p` is a valid position in `this`. + */ + private def setValue(b: Boolean, p: BitArray.Position): Unit = + val m = 1 << p.offsetInBucket + _bits = _bits.modifyAt( + p.bucket, + (e) => if b then e | m else e & ~m, + assumeUniqueness = true + ) + +} + +object BitArray { + + /** A position in a `BitArray`. + * + * @param bucket + * The bucket containing `this`. + * @param offsetInBucket + * The offset of `this` in its containing bucket. + */ + final class Position( + private[BitArray] val bucket: Int, + private[BitArray] val offsetInBucket: Int + ) { + + /** Creates a position from an index. */ + private[BitArray] def this(index: Int) = + this(index >> 5, index & 31) + + /** Returns the index corresponding to this position. */ + private def index: Int = + (bucket >> 5) + offsetInBucket + + /** Returns a copy of `this`. */ + def copy(): Position = + new Position(bucket, offsetInBucket) + + /** Returns `true` iff `this` and `other` have an equivalent value. */ + def eq(other: Position): Boolean = + (this.bucket == other.bucket) && (this.offsetInBucket == other.offsetInBucket) + + /** Hashes the salient parts of `self` into `hasher`. */ + def hashInto(hasher: Hasher): Hasher = + hasher.combine(bucket) + hasher.combine(offsetInBucket) + + } + + /** Creates an array with the given `bits`. */ + def apply[T](bits: Boolean*): BitArray = + var result = new BitArray(HyArray[Int](), 0) + for (b <- bits) result = result.append(b, assumeUniqueness = true) + result + +} + +given bitArrayPositionIsValue: Value[BitArray.Position] with { + + extension (self: BitArray.Position) { + + def copy(): BitArray.Position = + self.copy() + + def eq(other: BitArray.Position): Boolean = + self.eq(other) + + def hashInto(hasher: Hasher): Hasher = + self.hashInto(hasher) + + } + +} + +given bitArrayIsCollection: Collection[BitArray] with { + + type Element = Boolean + //given elementIsValue: Value[Boolean] = booleanIsValue + + type Position = BitArray.Position + given positionIsValue: Value[BitArray.Position] = bitArrayPositionIsValue + + extension (self: BitArray) { + + override def count: Int = + self.count + + def startPosition: BitArray.Position = + self.startPosition + + def endPosition: BitArray.Position = + self.endPosition + + def positionAfter(p: BitArray.Position): BitArray.Position = + self.positionAfter(p) + + def at(p: BitArray.Position): Boolean = + self.at(p) + + } + +} + +given bitArrayIsStringConvertible: StringConvertible[BitArray] with { + + extension (self: BitArray) + override def description: String = + var contents = mutable.StringBuilder() + self.forEach((e) => { contents += (if e then '1' else '0'); true }) + contents.mkString + +} diff --git a/tests/pos/hylolib/Collection.scala b/tests/pos/hylolib/Collection.scala new file mode 100644 index 000000000000..8dc41af40a65 --- /dev/null +++ b/tests/pos/hylolib/Collection.scala @@ -0,0 +1,281 @@ +//> using options -language:experimental.modularity -source future +package hylo + +/** A collection of elements accessible by their position. */ +trait Collection[Self] { + + /** The type of the elements in the collection. */ + type Element + given elementIsValue: Value[Element] = deferredSummon + + /** The type of a position in the collection. */ + type Position + given positionIsValue: Value[Position] + + extension (self: Self) { + + /** Returns `true` iff `self` is empty. */ + def isEmpty: Boolean = + startPosition `eq` endPosition + + /** Returns the number of elements in `self`. + * + * @complexity + * O(n) where n is the number of elements in `self`. + */ + def count: Int = + val e = endPosition + def _count(p: Position, n: Int): Int = + if p `eq` e then n else _count(self.positionAfter(p), n + 1) + _count(startPosition, 0) + + /** Returns the position of `self`'s first element', or `endPosition` if `self` is empty. + * + * @complexity + * O(1) + */ + def startPosition: Position + + /** Returns the "past the end" position in `self`, that is, the position immediately after the + * last element in `self`. + * + * @complexity + * O(1). + */ + def endPosition: Position + + /** Returns the position immediately after `p`. + * + * @requires + * `p` is a valid position in `self` different from `endPosition`. + * @complexity + * O(1). + */ + def positionAfter(p: Position): Position + + /** Accesses the element at `p`. + * + * @requires + * `p` is a valid position in `self` different from `endPosition`. + * @complexity + * O(1). + */ + def at(p: Position): Element + + /** Returns `true` iff `i` precedes `j`. + * + * @requires + * `i` and j` are valid positions in `self`. + * @complexity + * O(n) where n is the number of elements in `self`. + */ + def isBefore(i: Position, j: Position): Boolean = + val e = self.endPosition + if (i.eq(e)) { + false + } else if (j.eq(e)) { + true + } else { + def _isBefore(n: Position): Boolean = + if (n.eq(j)) { + true + } else if (n.eq(e)) { + false + } else { + _isBefore(self.positionAfter(n)) + } + _isBefore(self.positionAfter(i)) + } + + } + +} + +extension [Self](self: Self)(using s: Collection[Self]) { + + /** Returns the first element of `self` along with a slice containing the suffix after this + * element, or `None` if `self` is empty. + * + * @complexity + * O(1) + */ + def headAndTail: Option[(s.Element, Slice[Self])] = + if (self.isEmpty) { + None + } else { + val p = self.startPosition + val q = self.positionAfter(p) + val t = Slice(self, Range(q, self.endPosition, (a, b) => (a `eq` b) || self.isBefore(a, b))) + Some((self.at(p), t)) + } + + /** Applies `combine` on `partialResult` and each element of `self`, in order. + * + * @complexity + * O(n) where n is the number of elements in `self`. + */ + def reduce[T](partialResult: T, combine: (T, s.Element) => T): T = + val e = self.endPosition + def loop(p: s.Position, r: T): T = + if (p.eq(e)) { + r + } else { + loop(self.positionAfter(p), combine(r, self.at(p))) + } + loop(self.startPosition, partialResult) + + /** Applies `action` on each element of `self`, in order, until `action` returns `false`, and + * returns `false` iff `action` did. + * + * You can return `false` from `action` to emulate a `continue` statement as found in traditional + * imperative languages (e.g., C). + * + * @complexity + * O(n) where n is the number of elements in `self`. + */ + def forEach(action: (s.Element) => Boolean): Boolean = + val e = self.endPosition + def loop(p: s.Position): Boolean = + if (p.eq(e)) { + true + } else if (!action(self.at(p))) { + false + } else { + loop(self.positionAfter(p)) + } + loop(self.startPosition) + + /** Returns a collection with the elements of `self` transformed by `transform`, in order. + * + * @complexity + * O(n) where n is the number of elements in `self`. + */ + def map[T](using Value[T])(transform: (s.Element) => T): HyArray[T] = + self.reduce( + HyArray[T](), + (r, e) => r.append(transform(e), assumeUniqueness = true) + ) + + /** Returns a collection with the elements of `self` satisfying `isInclude`, in order. + * + * @complexity + * O(n) where n is the number of elements in `self`. + */ + def filter(isIncluded: (s.Element) => Boolean): HyArray[s.Element] = + self.reduce( + HyArray[s.Element](), + (r, e) => if (isIncluded(e)) then r.append(e, assumeUniqueness = true) else r + ) + + /** Returns `true` if `self` contains an element satisfying `predicate`. + * + * @complexity + * O(n) where n is the number of elements in `self`. + */ + def containsWhere(predicate: (s.Element) => Boolean): Boolean = + self.firstPositionWhere(predicate) != None + + /** Returns `true` if all elements in `self` satisfy `predicate`. + * + * @complexity + * O(n) where n is the number of elements in `self`. + */ + def allSatisfy(predicate: (s.Element) => Boolean): Boolean = + self.firstPositionWhere(predicate) == None + + /** Returns the position of the first element of `self` satisfying `predicate`, or `None` if no + * such element exists. + * + * @complexity + * O(n) where n is the number of elements in `self`. + */ + def firstPositionWhere(predicate: (s.Element) => Boolean): Option[s.Position] = + val e = self.endPosition + def loop(p: s.Position): Option[s.Position] = + if (p.eq(e)) { + None + } else if (predicate(self.at(p))) { + Some(p) + } else { + loop(self.positionAfter(p)) + } + loop(self.startPosition) + + /** Returns the minimum element in `self`, using `isLessThan` to compare elements. + * + * @complexity + * O(n) where n is the number of elements in `self`. + */ + def minElement(isLessThan: (s.Element, s.Element) => Boolean): Option[s.Element] = + self.leastElement(isLessThan) + + // NOTE: I can't find a reasonable way to call this method. + /** Returns the minimum element in `self`. + * + * @complexity + * O(n) where n is the number of elements in `self`. + */ + def minElement()(using Comparable[s.Element]): Option[s.Element] = + self.minElement(isLessThan = _ `lt` _) + + /** Returns the maximum element in `self`, using `isGreaterThan` to compare elements. + * + * @complexity + * O(n) where n is the number of elements in `self`. + */ + def maxElement(isGreaterThan: (s.Element, s.Element) => Boolean): Option[s.Element] = + self.leastElement(isGreaterThan) + + /** Returns the maximum element in `self`. + * + * @complexity + * O(n) where n is the number of elements in `self`. + */ + def maxElement()(using Comparable[s.Element]): Option[s.Element] = + self.maxElement(isGreaterThan = _ `gt` _) + + /** Returns the maximum element in `self`, using `isOrderedBefore` to compare elements. + * + * @complexity + * O(n) where n is the number of elements in `self`. + */ + def leastElement(isOrderedBefore: (s.Element, s.Element) => Boolean): Option[s.Element] = + if (self.isEmpty) { + None + } else { + val e = self.endPosition + def _least(p: s.Position, least: s.Element): s.Element = + if (p.eq(e)) { + least + } else { + val x = self.at(p) + val y = if isOrderedBefore(x, least) then x else least + _least(self.positionAfter(p), y) + } + + val b = self.startPosition + Some(_least(self.positionAfter(b), self.at(b))) + } + +} + +extension [Self](self: Self)(using + s: Collection[Self], + e: Value[s.Element] +) { + + /** Returns `true` if `self` contains the same elements as `other`, in the same order. */ + def elementsEqual[T](using o: Collection[T] { type Element = s.Element })(other: T): Boolean = + def loop(i: s.Position, j: o.Position): Boolean = + if (i `eq` self.endPosition) { + j `eq` other.endPosition + } else if (j `eq` other.endPosition) { + false + } else if (self.at(i) `neq` other.at(j)) { + false + } else { + loop(self.positionAfter(i), other.positionAfter(j)) + } + loop(self.startPosition, other.startPosition) + +} diff --git a/tests/pos/hylolib/CoreTraits.scala b/tests/pos/hylolib/CoreTraits.scala new file mode 100644 index 000000000000..01b2c5242af9 --- /dev/null +++ b/tests/pos/hylolib/CoreTraits.scala @@ -0,0 +1,57 @@ +package hylo + +/** A type whose instance can be treated as independent values. + * + * The data structure of and algorithms of Hylo's standard library operate "notional values" rather + * than arbitrary references. This trait defines the basis operations of all values. + */ +trait Value[Self] { + + extension (self: Self) { + + /** Returns a copy of `self`. */ + def copy(): Self + + /** Returns `true` iff `self` and `other` have an equivalent value. */ + def eq(other: Self): Boolean + + /** Hashes the salient parts of `self` into `hasher`. */ + def hashInto(hasher: Hasher): Hasher + + } + +} + +extension [Self: Value](self: Self) def neq(other: Self): Boolean = !self.eq(other) + +// ---------------------------------------------------------------------------- +// Comparable +// ---------------------------------------------------------------------------- + +trait Comparable[Self] extends Value[Self] { + + extension (self: Self) { + + /** Returns `true` iff `self` is ordered before `other`. */ + def lt(other: Self): Boolean + + /** Returns `true` iff `self` is ordered after `other`. */ + def gt(other: Self): Boolean = other.lt(self) + + /** Returns `true` iff `self` is equal to or ordered before `other`. */ + def le(other: Self): Boolean = !other.lt(self) + + /** Returns `true` iff `self` is equal to or ordered after `other`. */ + def ge(other: Self): Boolean = !self.lt(other) + + } + +} + +/** Returns the lesser of `x` and `y`. */ +def min[T: Comparable](x: T, y: T): T = + if y.lt(x) then y else x + +/** Returns the greater of `x` and `y`. */ +def max[T: Comparable](x: T, y: T): T = + if x.lt(y) then y else x diff --git a/tests/pos/hylolib/Hasher.scala b/tests/pos/hylolib/Hasher.scala new file mode 100644 index 000000000000..ef6813df6b60 --- /dev/null +++ b/tests/pos/hylolib/Hasher.scala @@ -0,0 +1,38 @@ +package hylo + +import scala.util.Random + +/** A universal hash function. */ +final class Hasher private (private val hash: Int = Hasher.offsetBasis) { + + /** Returns the computed hash value. */ + def finalizeHash(): Int = + hash + + /** Adds `n` to the computed hash value. */ + def combine(n: Int): Hasher = + var h = hash + h = h ^ n + h = h * Hasher.prime + new Hasher(h) +} + +object Hasher { + + private val offsetBasis = 0x811c9dc5 + private val prime = 0x01000193 + + /** A random seed ensuring different hashes across multiple runs. */ + private lazy val seed = scala.util.Random.nextInt() + + /** Creates an instance with the given `seed`. */ + def apply(): Hasher = + val h = new Hasher() + h.combine(seed) + h + + /** Returns the hash of `v`. */ + def hash[T: Value](v: T): Int = + v.hashInto(Hasher()).finalizeHash() + +} diff --git a/tests/pos/hylolib/HyArray.scala b/tests/pos/hylolib/HyArray.scala new file mode 100644 index 000000000000..98632dcb65bc --- /dev/null +++ b/tests/pos/hylolib/HyArray.scala @@ -0,0 +1,224 @@ +package hylo + +import java.util.Arrays +import scala.collection.mutable + +/** An ordered, random-access collection. */ +final class HyArray[Element] private (using + elementIsValue: Value[Element] +)( + private var _storage: scala.Array[AnyRef | Null] | Null, + private var _count: Int // NOTE: where do I document private fields +) { + + // NOTE: The fact that we need Array[AnyRef] is diappointing and difficult to discover + // The compiler error sent me on a wild goose chase with ClassTag. + + /** Returns `true` iff `this` is empty. */ + def isEmpty: Boolean = + _count == 0 + + /** Returns the number of elements in `this`. */ + def count: Int = + _count + + /** Returns the number of elements that `this` can contain before allocating new storage. */ + def capacity: Int = + if _storage == null then 0 else _storage.length + + /** Reserves enough storage to store `n` elements in `this`. */ + def reserveCapacity(n: Int, assumeUniqueness: Boolean = false): HyArray[Element] = + if (n <= capacity) { + this + } else { + var newCapacity = max(1, capacity) + while (newCapacity < n) { newCapacity = newCapacity << 1 } + + val newStorage = new scala.Array[AnyRef | Null](newCapacity) + val s = _storage.asInstanceOf[scala.Array[AnyRef | Null]] + var i = 0 + while (i < count) { + newStorage(i) = _storage(i).asInstanceOf[Element].copy().asInstanceOf[AnyRef] + i += 1 + } + + if (assumeUniqueness) { + _storage = newStorage + this + } else { + new HyArray(newStorage, count) + } + } + + /** Adds a new element at the end of the array. */ + def append(source: Element, assumeUniqueness: Boolean = false): HyArray[Element] = + val result = if assumeUniqueness && (count < capacity) then this else copy(count + 1) + result._storage(count) = source.asInstanceOf[AnyRef] + result._count += 1 + result + + // NOTE: Can't refine `C.Element` without renaming the generic parameter of `HyArray`. + // /** Adds the contents of `source` at the end of the array. */ + // def appendContents[C](using + // s: Collection[C] + // )( + // source: C { type Element = Element }, + // assumeUniqueness: Boolean = false + // ): HyArray[Element] = + // val result = if (assumeUniqueness) { this } else { copy(count + source.count) } + // source.reduce(result, (r, e) => r.append(e, assumeUniqueness = true)) + + /** Removes and returns the last element, or returns `None` if the array is empty. */ + def popLast(assumeUniqueness: Boolean = false): (HyArray[Element], Option[Element]) = + if (isEmpty) { + (this, None) + } else { + val result = if assumeUniqueness then this else copy() + result._count -= 1 + (result, Some(result._storage(result._count).asInstanceOf[Element])) + } + + /** Removes all elements in the array, keeping allocated storage iff `keepStorage` is true. */ + def removeAll( + keepStorage: Boolean = false, + assumeUniqueness: Boolean = false + ): HyArray[Element] = + if (isEmpty) { + this + } else if (keepStorage) { + val result = if assumeUniqueness then this else copy() + Arrays.fill(result._storage, null) + result._count = 0 + result + } else { + HyArray() + } + + /** Accesses the element at `p`. + * + * @requires + * `p` is a valid position in `self` different from `endPosition`. + * @complexity + * O(1). + */ + def at(p: Int): Element = + _storage(p).asInstanceOf[Element] + + /** Calls `transform` on the element at `p` to update its value. + * + * @requires + * `p` is a valid position in `self` different from `endPosition`. + * @complexity + * O(1). + */ + def modifyAt( + p: Int, + transform: (Element) => Element, + assumeUniqueness: Boolean = false + ): HyArray[Element] = + val result = if assumeUniqueness then this else copy() + result._storage(p) = transform(at(p)).asInstanceOf[AnyRef] + result + + /** Returns a textual description of `this`. */ + override def toString: String = + var s = "[" + var i = 0 + while (i < count) { + if (i > 0) { s += ", " } + s += s"${at(i)}" + i += 1 + } + s + "]" + + /** Returns an independent copy of `this`, capable of storing `minimumCapacity` elements before + * allocating new storage. + */ + def copy(minimumCapacity: Int = 0): HyArray[Element] = + if (minimumCapacity > capacity) { + // If the requested capacity on the copy is greater than what we have, `reserveCapacity` will + // create an independent value. + reserveCapacity(minimumCapacity) + } else { + val clone = HyArray[Element]().reserveCapacity(max(minimumCapacity, count)) + var i = 0 + while (i < count) { + clone._storage(i) = _storage(i).asInstanceOf[Element].copy().asInstanceOf[AnyRef] + i += 1 + } + clone._count = count + clone + } + +} + +object HyArray { + + /** Creates an array with the given `elements`. */ + def apply[T](using t: Value[T])(elements: T*): HyArray[T] = + var a = new HyArray[T](null, 0) + for (e <- elements) a = a.append(e, assumeUniqueness = true) + a + +} + +given hyArrayIsValue[T](using tIsValue: Value[T]): Value[HyArray[T]] with { + + extension (self: HyArray[T]) { + + def copy(): HyArray[T] = + self.copy() + + def eq(other: HyArray[T]): Boolean = + self.elementsEqual(other) + + def hashInto(hasher: Hasher): Hasher = + self.reduce(hasher, (h, e) => e.hashInto(h)) + + } + +} + +given hyArrayIsCollection[T](using tIsValue: Value[T]): Collection[HyArray[T]] with { + + type Element = T + //given elementIsValue: Value[T] = tIsValue + + type Position = Int + given positionIsValue: Value[Int] = intIsValue + + extension (self: HyArray[T]) { + + // NOTE: Having to explicitly override means that primary declaration can't automatically + // specialize trait requirements. + override def isEmpty: Boolean = self.isEmpty + + override def count: Int = self.count + + def startPosition = 0 + + def endPosition = self.count + + def positionAfter(p: Int) = p + 1 + + def at(p: Int) = self.at(p) + + } + +} + +// NOTE: This should work. +// given hyArrayIsStringConvertible[T](using +// tIsValue: Value[T], +// tIsStringConvertible: StringConvertible[T] +// ): StringConvertible[HyArray[T]] with { +// +// given Collection[HyArray[T]] = hyArrayIsCollection[T] +// +// extension (self: HyArray[T]) +// override def description: String = +// var contents = mutable.StringBuilder() +// self.forEach((e) => { contents ++= e.description; true }) +// s"[${contents.mkString(", ")}]" +// +// } diff --git a/tests/pos/hylolib/Integers.scala b/tests/pos/hylolib/Integers.scala new file mode 100644 index 000000000000..b9bc203a88ea --- /dev/null +++ b/tests/pos/hylolib/Integers.scala @@ -0,0 +1,58 @@ +package hylo + +given booleanIsValue: Value[Boolean] with { + + extension (self: Boolean) { + + def copy(): Boolean = + // Note: Scala's `Boolean` has value semantics already. + self + + def eq(other: Boolean): Boolean = + self == other + + def hashInto(hasher: Hasher): Hasher = + hasher.combine(if self then 1 else 0) + + } + +} + +given intIsValue: Value[Int] with { + + extension (self: Int) { + + def copy(): Int = + // Note: Scala's `Int` has value semantics already. + self + + def eq(other: Int): Boolean = + self == other + + def hashInto(hasher: Hasher): Hasher = + hasher.combine(self) + + } + +} + +given intIsComparable: Comparable[Int] with { + + extension (self: Int) { + + def copy(): Int = + self + + def eq(other: Int): Boolean = + self == other + + def hashInto(hasher: Hasher): Hasher = + hasher.combine(self) + + def lt(other: Int): Boolean = self < other + + } + +} + +given intIsStringConvertible: StringConvertible[Int] with {} diff --git a/tests/pos/hylolib/Range.scala b/tests/pos/hylolib/Range.scala new file mode 100644 index 000000000000..1f597652ead1 --- /dev/null +++ b/tests/pos/hylolib/Range.scala @@ -0,0 +1,37 @@ +package hylo + +/** A half-open interval from a lower bound up to, but not including, an uppor bound. */ +final class Range[Bound] private (val lowerBound: Bound, val upperBound: Bound) { + + /** Returns a textual description of `this`. */ + override def toString: String = + s"[${lowerBound}, ${upperBound})" + +} + +object Range { + + /** Creates a half-open interval [`lowerBound`, `upperBound`), using `isLessThanOrEqual` to ensure + * that the bounds are well-formed. + * + * @requires + * `lowerBound` is lesser than or equal to `upperBound`. + */ + def apply[Bound]( + lowerBound: Bound, + upperBound: Bound, + isLessThanOrEqual: (Bound, Bound) => Boolean + ) = + require(isLessThanOrEqual(lowerBound, upperBound)) + new Range(lowerBound, upperBound) + + /** Creates a half-open interval [`lowerBound`, `upperBound`). + * + * @requires + * `lowerBound` is lesser than or equal to `upperBound`. + */ + def apply[Bound](lowerBound: Bound, upperBound: Bound)(using Comparable[Bound]) = + require(lowerBound `le` upperBound) + new Range(lowerBound, upperBound) + +} diff --git a/tests/pos/hylolib/Slice.scala b/tests/pos/hylolib/Slice.scala new file mode 100644 index 000000000000..57cdb38f6e53 --- /dev/null +++ b/tests/pos/hylolib/Slice.scala @@ -0,0 +1,49 @@ +package hylo + +/** A view into a collection. */ +final class Slice[Base](using + val b: Collection[Base] +)( + val base: Base, + val bounds: Range[b.Position] +) { + + /** Returns `true` iff `this` is empty. */ + def isEmpty: Boolean = + bounds.lowerBound.eq(bounds.upperBound) + + def startPosition: b.Position = + bounds.lowerBound + + def endPosition: b.Position = + bounds.upperBound + + def positionAfter(p: b.Position): b.Position = + base.positionAfter(p) + + def at(p: b.Position): b.Element = + base.at(p) + +} + +given sliceIsCollection[T](using c: Collection[T]): Collection[Slice[T]] with { + + type Element = c.Element + //given elementIsValue: Value[Element] = c.elementIsValue + + type Position = c.Position + given positionIsValue: Value[Position] = c.positionIsValue + + extension (self: Slice[T]) { + + def startPosition = self.bounds.lowerBound.asInstanceOf[Position] // NOTE: Ugly hack + + def endPosition = self.bounds.upperBound.asInstanceOf[Position] + + def positionAfter(p: Position) = self.base.positionAfter(p) + + def at(p: Position) = self.base.at(p) + + } + +} diff --git a/tests/pos/hylolib/StringConvertible.scala b/tests/pos/hylolib/StringConvertible.scala new file mode 100644 index 000000000000..0702f79f2794 --- /dev/null +++ b/tests/pos/hylolib/StringConvertible.scala @@ -0,0 +1,14 @@ +package hylo + +/** A type whose instances can be described by a character string. */ +trait StringConvertible[Self] { + + extension (self: Self) { + + /** Returns a textual description of `self`. */ + def description: String = + self.toString + + } + +} From aba63cdd810c58222b455fa98bf9f15cc077e7ac Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Jan 2024 22:55:29 +0100 Subject: [PATCH 19/63] Allow context bounds in type declarations Expand them to givens with deferred summons # Conflicts: # compiler/src/dotty/tools/dotc/ast/Desugar.scala --- .../src/dotty/tools/dotc/ast/Desugar.scala | 75 +++++++++++-------- .../src/dotty/tools/dotc/core/Flags.scala | 1 + .../dotty/tools/dotc/parsing/Parsers.scala | 59 ++++++++------- .../src/dotty/tools/dotc/typer/Typer.scala | 4 +- docs/_docs/internals/syntax.md | 8 +- tests/pos/deferredSummon.scala | 21 +++++- tests/pos/hylolib-extract.scala | 3 +- tests/pos/hylolib/AnyCollection.scala | 3 - tests/pos/hylolib/BitArray.scala | 3 - tests/pos/hylolib/Collection.scala | 6 +- tests/pos/hylolib/HyArray.scala | 3 - tests/pos/hylolib/Slice.scala | 3 - 12 files changed, 103 insertions(+), 86 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 6dc31a2fdfc7..9ec0af4d0b11 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -226,44 +226,48 @@ object desugar { private def defDef(meth: DefDef, isPrimaryConstructor: Boolean = false)(using Context): Tree = addDefaultGetters(elimContextBounds(meth, isPrimaryConstructor)) + private def desugarContextBounds( + tname: TypeName, rhs: Tree, + evidenceBuf: ListBuffer[ValDef], + flags: FlagSet, + freshName: => TermName)(using Context): Tree = rhs match + case ContextBounds(tbounds, cxbounds) => + var useParamName = Feature.enabled(Feature.modularity) + for bound <- cxbounds do + val evidenceName = bound match + case ContextBoundTypeTree(_, _, ownName) if !ownName.isEmpty => ownName + case _ if useParamName => tname.toTermName + case _ => freshName + useParamName = false + val evidenceParam = ValDef(evidenceName, bound, EmptyTree).withFlags(flags) + evidenceParam.pushAttachment(ContextBoundParam, ()) + evidenceBuf += evidenceParam + tbounds + case LambdaTypeTree(tparams, body) => + cpy.LambdaTypeTree(rhs)(tparams, + desugarContextBounds(tname, body, evidenceBuf, flags, freshName)) + case _ => + rhs + end desugarContextBounds + private def elimContextBounds(meth: DefDef, isPrimaryConstructor: Boolean)(using Context): DefDef = val DefDef(_, paramss, tpt, rhs) = meth val evidenceParamBuf = ListBuffer[ValDef]() var seenContextBounds: Int = 0 - - def desugarContextBounds(tparam: TypeDef, rhs: Tree): Tree = rhs match - case ContextBounds(tbounds, cxbounds) => - val iflag = if Feature.sourceVersion.isAtLeast(`future`) then Given else Implicit - val flags = if isPrimaryConstructor then iflag | LocalParamAccessor else iflag | Param - var useParamName = Feature.enabled(Feature.modularity) - for bound <- cxbounds do - val paramName = - bound match - case ContextBoundTypeTree(_, _, ownName) if !ownName.isEmpty => - ownName - case _ if useParamName => - tparam.name.toTermName - case _ => - seenContextBounds += 1 // Start at 1 like FreshNameCreator. - ContextBoundParamName(EmptyTermName, seenContextBounds) - // Just like with `makeSyntheticParameter` on nameless parameters of - // using clauses, we only need names that are unique among the - // parameters of the method since shadowing does not affect - // implicit resolution in Scala 3. - useParamName = false - val evidenceParam = ValDef(paramName, bound, EmptyTree).withFlags(flags) - evidenceParam.pushAttachment(ContextBoundParam, ()) - evidenceParamBuf += evidenceParam - tbounds - case LambdaTypeTree(tparams, body) => - cpy.LambdaTypeTree(rhs)(tparams, desugarContextBounds(tparam, body)) - case _ => - rhs - end desugarContextBounds + def freshName = + seenContextBounds += 1 // Start at 1 like FreshNameCreator. + ContextBoundParamName(EmptyTermName, seenContextBounds) + // Just like with `makeSyntheticParameter` on nameless parameters of + // using clauses, we only need names that are unique among the + // parameters of the method since shadowing does not affect + // implicit resolution in Scala 3. val paramssNoContextBounds = + val iflag = if Feature.sourceVersion.isAtLeast(`future`) then Given else Implicit + val flags = if isPrimaryConstructor then iflag | LocalParamAccessor else iflag | Param mapParamss(paramss) { - tparam => cpy.TypeDef(tparam)(rhs = desugarContextBounds(tparam, tparam.rhs)) + tparam => cpy.TypeDef(tparam)(rhs = + desugarContextBounds(tparam.name, tparam.rhs, evidenceParamBuf, flags, freshName)) }(identity) rhs match @@ -459,6 +463,13 @@ object desugar { Apply(fn, params.map(refOfDef)) } + def typeDef(tdef: TypeDef)(using Context): Tree = + val evidenceBuf = new ListBuffer[ValDef] + val result = cpy.TypeDef(tdef)(rhs = + desugarContextBounds(tdef.name, tdef.rhs, evidenceBuf, + (tdef.mods.flags.toTermFlags & AccessFlags) | DeferredGivenFlags, EmptyTermName)) + if evidenceBuf.isEmpty then result else Thicket(result :: evidenceBuf.toList) + /** The expansion of a class definition. See inline comments for what is involved */ def classDef(cdef: TypeDef)(using Context): Tree = { val impl @ Template(constr0, _, self, _) = cdef.rhs: @unchecked @@ -1409,7 +1420,7 @@ object desugar { case tree: TypeDef => if (tree.isClassDef) classDef(tree) else if (ctx.mode.isQuotedPattern) quotedPatternTypeDef(tree) - else tree + else typeDef(tree) case tree: DefDef => if (tree.name.isConstructorName) tree // was already handled by enclosing classDef else defDef(tree) diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index 0764d4149468..0549245d6fcd 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -570,6 +570,7 @@ object Flags { val DeferredOrLazyOrMethod: FlagSet = Deferred | Lazy | Method val DeferredOrTermParamOrAccessor: FlagSet = Deferred | ParamAccessor | TermParam // term symbols without right-hand sides val DeferredOrTypeParam: FlagSet = Deferred | TypeParam // type symbols without right-hand sides + val DeferredGivenFlags = Deferred | Given | HasDefault val EnumValue: FlagSet = Enum | StableRealizable // A Scala enum value val FinalOrInline: FlagSet = Final | Inline val FinalOrModuleClass: FlagSet = Final | ModuleClass // A module class or a final class diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 9bc996ede196..77cba5b6f1d0 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2160,9 +2160,9 @@ object Parsers { if (in.token == tok) { in.nextToken(); toplevelTyp() } else EmptyTree - /** TypeParamBounds ::= TypeBounds {`<%' Type} [`:` ContextBounds] + /** TypeAndCtxBounds ::= TypeBounds {`<%' Type} [`:` ContextBounds] */ - def typeParamBounds(pname: TypeName): Tree = { + def typeAndCtxBounds(pname: TypeName): Tree = { val t = typeBounds() val cbs = contextBounds(pname) if (cbs.isEmpty) t @@ -3378,7 +3378,7 @@ object Parsers { } else ident().toTypeName val hkparams = typeParamClauseOpt(ParamOwner.Type) - val bounds = if (isAbstractOwner) typeBounds() else typeParamBounds(name) + val bounds = if (isAbstractOwner) typeBounds() else typeAndCtxBounds(name) TypeDef(name, lambdaAbstract(hkparams, bounds)).withMods(mods) } } @@ -3889,14 +3889,16 @@ object Parsers { argumentExprss(mkApply(Ident(nme.CONSTRUCTOR), argumentExprs())) } - /** TypeDef ::= id [TypeParamClause] {FunParamClause} TypeBounds [‘=’ Type] + /** TypeDef ::= id [TypeParamClause] {FunParamClause} TypeAndCtxBounds [‘=’ Type] */ def typeDefOrDcl(start: Offset, mods: Modifiers): Tree = { newLinesOpt() atSpan(start, nameStart) { val nameIdent = typeIdent() + val tname = nameIdent.name.asTypeName val tparams = typeParamClauseOpt(ParamOwner.Type) val vparamss = funParamClauses() + def makeTypeDef(rhs: Tree): Tree = { val rhs1 = lambdaAbstractAll(tparams :: vparamss, rhs) val tdef = TypeDef(nameIdent.name.toTypeName, rhs1) @@ -3904,36 +3906,37 @@ object Parsers { tdef.pushAttachment(Backquoted, ()) finalizeDef(tdef, mods, start) } + in.token match { case EQUALS => in.nextToken() makeTypeDef(toplevelTyp()) case SUBTYPE | SUPERTYPE => - val bounds = typeBounds() - if (in.token == EQUALS) { - val eqOffset = in.skipToken() - var rhs = toplevelTyp() - rhs match { - case mtt: MatchTypeTree => - bounds match { - case TypeBoundsTree(EmptyTree, upper, _) => - rhs = MatchTypeTree(upper, mtt.selector, mtt.cases) - case _ => - syntaxError(em"cannot combine lower bound and match type alias", eqOffset) - } - case _ => - if mods.is(Opaque) then - rhs = TypeBoundsTree(bounds.lo, bounds.hi, rhs) - else - syntaxError(em"cannot combine bound and alias", eqOffset) - } - makeTypeDef(rhs) - } - else makeTypeDef(bounds) + typeAndCtxBounds(tname) match + case bounds: TypeBoundsTree if in.token == EQUALS => + val eqOffset = in.skipToken() + var rhs = toplevelTyp() + rhs match { + case mtt: MatchTypeTree => + bounds match { + case TypeBoundsTree(EmptyTree, upper, _) => + rhs = MatchTypeTree(upper, mtt.selector, mtt.cases) + case _ => + syntaxError(em"cannot combine lower bound and match type alias", eqOffset) + } + case _ => + if mods.is(Opaque) then + rhs = TypeBoundsTree(bounds.lo, bounds.hi, rhs) + else + syntaxError(em"cannot combine bound and alias", eqOffset) + } + makeTypeDef(rhs) + case bounds => makeTypeDef(bounds) case SEMI | NEWLINE | NEWLINES | COMMA | RBRACE | OUTDENT | EOF => - makeTypeDef(typeBounds()) - case _ if (staged & StageKind.QuotedPattern) != 0 => - makeTypeDef(typeBounds()) + makeTypeDef(typeAndCtxBounds(tname)) + case _ if (staged & StageKind.QuotedPattern) != 0 + || in.featureEnabled(Feature.modularity) && in.isColon => + makeTypeDef(typeAndCtxBounds(tname)) case _ => syntaxErrorOrIncomplete(ExpectedTypeBoundOrEquals(in.token)) return EmptyTree // return to avoid setting the span to EmptyTree diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index ad86535f4eb8..e7466e822909 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2578,7 +2578,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val rhs1 = vdef.rhs match { case rhs @ Ident(nme.WILDCARD) => rhs.withType(tpt1.tpe) - case Ident(nme.deferredSummon) if sym.isAllOf(Given | Deferred | HasDefault, butNot = Param) => + case Ident(nme.deferredSummon) if sym.isAllOf(DeferredGivenFlags, butNot = Param) => EmptyTree case rhs => typedExpr(rhs, tpt1.tpe.widenExpr) @@ -2848,7 +2848,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val givenImpls = cls.thisType.implicitMembers - .filter(_.symbol.isAllOf(Given | Deferred | HasDefault, butNot = Param)) + .filter(_.symbol.isAllOf(DeferredGivenFlags, butNot = Param)) .map(givenImpl) body ++ givenImpls end implementDeferredGivens diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index 37b6110a6df7..0245ce802243 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -221,7 +221,7 @@ IntoTargetType ::= Type TypeArgs ::= ‘[’ Types ‘]’ ts Refinement ::= :<<< [RefineDcl] {semi [RefineDcl]} >>> ds TypeBounds ::= [‘>:’ Type] [‘<:’ Type] TypeBoundsTree(lo, hi) -TypeParamBounds ::= TypeBounds [‘:’ ContextBounds] ContextBounds(typeBounds, tps) +TypeAndCtxBounds ::= TypeBounds [‘:’ ContextBounds] ContextBounds(typeBounds, tps) ContextBounds ::= ContextBound | '{' ContextBound {',' ContextBound} '}' ContextBound ::= Type ['as' id] Types ::= Type {‘,’ Type} @@ -354,7 +354,7 @@ ArgumentPatterns ::= ‘(’ [Patterns] ‘)’ ```ebnf ClsTypeParamClause::= ‘[’ ClsTypeParam {‘,’ ClsTypeParam} ‘]’ ClsTypeParam ::= {Annotation} [‘+’ | ‘-’] TypeDef(Modifiers, name, tparams, bounds) - id [HkTypeParamClause] TypeParamBounds Bound(below, above, context) + id [HkTypeParamClause] TypeAndCtxBounds Bound(below, above, context) TypTypeParamClause::= ‘[’ TypTypeParam {‘,’ TypTypeParam} ‘]’ TypTypeParam ::= {Annotation} id [HkTypeParamClause] TypeBounds @@ -379,7 +379,7 @@ TypelessClause ::= DefTermParamClause | UsingParamClause DefTypeParamClause::= [nl] ‘[’ DefTypeParam {‘,’ DefTypeParam} ‘]’ -DefTypeParam ::= {Annotation} id [HkTypeParamClause] TypeParamBounds +DefTypeParam ::= {Annotation} id [HkTypeParamClause] TypeAndCtxBounds DefTermParamClause::= [nl] ‘(’ [DefTermParams] ‘)’ UsingParamClause ::= [nl] ‘(’ ‘using’ (DefTermParams | FunArgTypes) ‘)’ DefImplicitClause ::= [nl] ‘(’ ‘implicit’ DefTermParams ‘)’ @@ -449,7 +449,7 @@ PatDef ::= ids [‘:’ Type] [‘=’ Expr] DefDef ::= DefSig [‘:’ Type] [‘=’ Expr] DefDef(_, name, paramss, tpe, expr) | ‘this’ TypelessClauses [DefImplicitClause] ‘=’ ConstrExpr DefDef(_, , vparamss, EmptyTree, expr | Block) DefSig ::= id [DefParamClauses] [DefImplicitClause] -TypeDef ::= id [TypeParamClause] {FunParamClause} TypeBounds TypeDefTree(_, name, tparams, bound +TypeDef ::= id [TypeParamClause] {FunParamClause} TypeAndCtxBounds TypeDefTree(_, name, tparams, bound [‘=’ Type] TmplDef ::= ([‘case’] ‘class’ | ‘trait’) ClassDef diff --git a/tests/pos/deferredSummon.scala b/tests/pos/deferredSummon.scala index 34455a46e137..f9e8953cee6e 100644 --- a/tests/pos/deferredSummon.scala +++ b/tests/pos/deferredSummon.scala @@ -8,11 +8,15 @@ trait A: given Elem is Ord = deferredSummon def foo = summon[Elem is Ord] +trait B: + type Elem: Ord + def foo = summon[Elem is Ord] + object Inst: given Int is Ord: def less(x: Int, y: Int) = x < y -object Test: +object Test1: import Inst.given class C extends A: type Elem = Int @@ -21,9 +25,22 @@ object Test: given A: type Elem = Int -class D[T: Ord] extends A: +class D1[T: Ord] extends B: + type Elem = T + +object Test2: + import Inst.given + class C extends B: + type Elem = Int + object E extends B: + type Elem = Int + given B: + type Elem = Int + +class D2[T: Ord] extends B: type Elem = T + diff --git a/tests/pos/hylolib-extract.scala b/tests/pos/hylolib-extract.scala index 2cb171bc99fd..3c16a0420a55 100644 --- a/tests/pos/hylolib-extract.scala +++ b/tests/pos/hylolib-extract.scala @@ -7,8 +7,7 @@ trait Value[Self] trait Collection[Self]: /** The type of the elements in the collection. */ - type Element - given elementIsValue: Value[Element] = deferredSummon + type Element: Value class BitArray diff --git a/tests/pos/hylolib/AnyCollection.scala b/tests/pos/hylolib/AnyCollection.scala index 55e453d6dc87..1a44344d0e51 100644 --- a/tests/pos/hylolib/AnyCollection.scala +++ b/tests/pos/hylolib/AnyCollection.scala @@ -45,10 +45,7 @@ object AnyCollection { given anyCollectionIsCollection[T](using tIsValue: Value[T]): Collection[AnyCollection[T]] with { type Element = T - //given elementIsValue: Value[Element] = tIsValue - type Position = AnyValue - given positionIsValue: Value[Position] = anyValueIsValue extension (self: AnyCollection[T]) { diff --git a/tests/pos/hylolib/BitArray.scala b/tests/pos/hylolib/BitArray.scala index 485f30472847..3a0b4658f747 100644 --- a/tests/pos/hylolib/BitArray.scala +++ b/tests/pos/hylolib/BitArray.scala @@ -338,10 +338,7 @@ given bitArrayPositionIsValue: Value[BitArray.Position] with { given bitArrayIsCollection: Collection[BitArray] with { type Element = Boolean - //given elementIsValue: Value[Boolean] = booleanIsValue - type Position = BitArray.Position - given positionIsValue: Value[BitArray.Position] = bitArrayPositionIsValue extension (self: BitArray) { diff --git a/tests/pos/hylolib/Collection.scala b/tests/pos/hylolib/Collection.scala index 8dc41af40a65..073a99cdd16b 100644 --- a/tests/pos/hylolib/Collection.scala +++ b/tests/pos/hylolib/Collection.scala @@ -5,12 +5,10 @@ package hylo trait Collection[Self] { /** The type of the elements in the collection. */ - type Element - given elementIsValue: Value[Element] = deferredSummon + type Element: Value /** The type of a position in the collection. */ - type Position - given positionIsValue: Value[Position] + type Position: Value as positionIsValue extension (self: Self) { diff --git a/tests/pos/hylolib/HyArray.scala b/tests/pos/hylolib/HyArray.scala index 98632dcb65bc..9347f7eb12cc 100644 --- a/tests/pos/hylolib/HyArray.scala +++ b/tests/pos/hylolib/HyArray.scala @@ -182,10 +182,7 @@ given hyArrayIsValue[T](using tIsValue: Value[T]): Value[HyArray[T]] with { given hyArrayIsCollection[T](using tIsValue: Value[T]): Collection[HyArray[T]] with { type Element = T - //given elementIsValue: Value[T] = tIsValue - type Position = Int - given positionIsValue: Value[Int] = intIsValue extension (self: HyArray[T]) { diff --git a/tests/pos/hylolib/Slice.scala b/tests/pos/hylolib/Slice.scala index 57cdb38f6e53..2289ac2a085b 100644 --- a/tests/pos/hylolib/Slice.scala +++ b/tests/pos/hylolib/Slice.scala @@ -29,10 +29,7 @@ final class Slice[Base](using given sliceIsCollection[T](using c: Collection[T]): Collection[Slice[T]] with { type Element = c.Element - //given elementIsValue: Value[Element] = c.elementIsValue - type Position = c.Position - given positionIsValue: Value[Position] = c.positionIsValue extension (self: Slice[T]) { From abca391ea77bdfc0876bf619e8814b61f8ba5f71 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Jan 2024 23:03:57 +0100 Subject: [PATCH 20/63] Tweak position where synthetic evidence parameters are added Make sure that any references to a synthetic evidence parameter come after that parameter is introduced. --- .../src/dotty/tools/dotc/ast/Desugar.scala | 41 +++++++++++++------ tests/pos/dep-context-bounds.scala | 17 ++++++++ tests/pos/hylolib/AnyCollection.scala | 4 +- tests/pos/hylolib/AnyValue.scala | 2 +- tests/pos/hylolib/Collection.scala | 9 ++-- tests/pos/hylolib/HyArray.scala | 11 +++-- tests/pos/hylolib/Range.scala | 2 +- tests/pos/hylolib/Slice.scala | 6 +-- 8 files changed, 61 insertions(+), 31 deletions(-) create mode 100644 tests/pos/dep-context-bounds.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 9ec0af4d0b11..af1b017d718d 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -407,21 +407,38 @@ object desugar { (Nil, tree) /** Add all evidence parameters in `params` as implicit parameters to `meth`. - * If the parameters of `meth` end in an implicit parameter list or using clause, - * evidence parameters are added in front of that list. Otherwise they are added - * as a separate parameter clause. + * The position of the added parameters is determined as follows: + * + * - If there is an existing parameter list that refers to one of the added + * parameters in one of its parameter types, add the new parameters + * in front of the first such parameter list. + * - Otherwise, if the last parameter list consists implicit or using parameters, + * join the new parameters in front of this parameter list, creating one + * parameter list (this is equilavent to Scala 2's scheme). + * - Otherwise, add the new parameter list at the end as a separate parameter clause. */ private def addEvidenceParams(meth: DefDef, params: List[ValDef])(using Context): DefDef = - params match + if params.isEmpty then return meth + + val boundNames = params.map(_.name).toSet + + def references(vdef: ValDef): Boolean = + vdef.tpt.existsSubTree: + case Ident(name: TermName) => boundNames.contains(name) + case _ => false + + def recur(mparamss: List[ParamClause]): List[ParamClause] = mparamss match + case ValDefs(mparams) :: _ if mparams.exists(references) => + params :: mparamss + case ValDefs(mparams @ (mparam :: _)) :: Nil if mparam.mods.isOneOf(GivenOrImplicit) => + (params ++ mparams) :: Nil + case mparams :: mparamss1 => + mparams :: recur(mparamss1) case Nil => - meth - case evidenceParams => - val paramss1 = meth.paramss.reverse match - case ValDefs(vparams @ (vparam :: _)) :: rparamss if vparam.mods.isOneOf(GivenOrImplicit) => - ((evidenceParams ++ vparams) :: rparamss).reverse - case _ => - meth.paramss :+ evidenceParams - cpy.DefDef(meth)(paramss = paramss1) + params :: Nil + + cpy.DefDef(meth)(paramss = recur(meth.paramss)) + end addEvidenceParams /** The parameters generated from the contextual bounds of `meth`, as generated by `desugar.defDef` */ private def evidenceParams(meth: DefDef)(using Context): List[ValDef] = diff --git a/tests/pos/dep-context-bounds.scala b/tests/pos/dep-context-bounds.scala new file mode 100644 index 000000000000..c724d92e9809 --- /dev/null +++ b/tests/pos/dep-context-bounds.scala @@ -0,0 +1,17 @@ +//> using options -language:experimental.modularity -source future +trait A: + type Self + +object Test1: + def foo[X: A](x: X.Self) = ??? + + def bar[X: A](a: Int) = ??? + + def baz[X: A](a: Int)(using String) = ??? + +object Test2: + def foo[X: A as x](a: x.Self) = ??? + + def bar[X: A as x](a: Int) = ??? + + def baz[X: A as x](a: Int)(using String) = ??? diff --git a/tests/pos/hylolib/AnyCollection.scala b/tests/pos/hylolib/AnyCollection.scala index 1a44344d0e51..50f4313e46ce 100644 --- a/tests/pos/hylolib/AnyCollection.scala +++ b/tests/pos/hylolib/AnyCollection.scala @@ -14,7 +14,7 @@ final class AnyCollection[Element] private ( object AnyCollection { /** Creates an instance forwarding its operations to `base`. */ - def apply[Base](using b: Collection[Base])(base: Base): AnyCollection[b.Element] = + def apply[Base: Collection as b](base: Base): AnyCollection[b.Element] = // NOTE: This evidence is redefined so the compiler won't report ambiguity between `intIsValue` // and `anyValueIsValue` when the method is called on a collection of `Int`s. None of these // choices is even correct! Note also that the ambiguity is suppressed if the constructor of @@ -42,7 +42,7 @@ object AnyCollection { } -given anyCollectionIsCollection[T](using tIsValue: Value[T]): Collection[AnyCollection[T]] with { +given anyCollectionIsCollection[T: Value]: Collection[AnyCollection[T]] with { type Element = T type Position = AnyValue diff --git a/tests/pos/hylolib/AnyValue.scala b/tests/pos/hylolib/AnyValue.scala index b9d39869c09a..21f2965e102e 100644 --- a/tests/pos/hylolib/AnyValue.scala +++ b/tests/pos/hylolib/AnyValue.scala @@ -44,7 +44,7 @@ final class AnyValue private ( object AnyValue { /** Creates an instance wrapping `wrapped`. */ - def apply[T](using Value[T])(wrapped: T): AnyValue = + def apply[T: Value](wrapped: T): AnyValue = def copy(a: AnyRef): AnyValue = AnyValue(a.asInstanceOf[Ref[T]].value.copy()) diff --git a/tests/pos/hylolib/Collection.scala b/tests/pos/hylolib/Collection.scala index 073a99cdd16b..2fc04f02b9ac 100644 --- a/tests/pos/hylolib/Collection.scala +++ b/tests/pos/hylolib/Collection.scala @@ -89,7 +89,7 @@ trait Collection[Self] { } -extension [Self](self: Self)(using s: Collection[Self]) { +extension [Self: Collection as s](self: Self) { /** Returns the first element of `self` along with a slice containing the suffix after this * element, or `None` if `self` is empty. @@ -148,7 +148,7 @@ extension [Self](self: Self)(using s: Collection[Self]) { * @complexity * O(n) where n is the number of elements in `self`. */ - def map[T](using Value[T])(transform: (s.Element) => T): HyArray[T] = + def map[T: Value](transform: (s.Element) => T): HyArray[T] = self.reduce( HyArray[T](), (r, e) => r.append(transform(e), assumeUniqueness = true) @@ -257,9 +257,8 @@ extension [Self](self: Self)(using s: Collection[Self]) { } -extension [Self](self: Self)(using - s: Collection[Self], - e: Value[s.Element] +extension [Self: Collection as s](self: Self)(using + Value[s.Element] ) { /** Returns `true` if `self` contains the same elements as `other`, in the same order. */ diff --git a/tests/pos/hylolib/HyArray.scala b/tests/pos/hylolib/HyArray.scala index 9347f7eb12cc..0fff45e744ec 100644 --- a/tests/pos/hylolib/HyArray.scala +++ b/tests/pos/hylolib/HyArray.scala @@ -1,12 +1,11 @@ +//> using options -language:experimental.modularity -source future package hylo import java.util.Arrays import scala.collection.mutable /** An ordered, random-access collection. */ -final class HyArray[Element] private (using - elementIsValue: Value[Element] -)( +final class HyArray[Element: Value as elementIsCValue]( private var _storage: scala.Array[AnyRef | Null] | Null, private var _count: Int // NOTE: where do I document private fields ) { @@ -155,14 +154,14 @@ final class HyArray[Element] private (using object HyArray { /** Creates an array with the given `elements`. */ - def apply[T](using t: Value[T])(elements: T*): HyArray[T] = + def apply[T: Value](elements: T*): HyArray[T] = var a = new HyArray[T](null, 0) for (e <- elements) a = a.append(e, assumeUniqueness = true) a } -given hyArrayIsValue[T](using tIsValue: Value[T]): Value[HyArray[T]] with { +given [T: Value] => Value[HyArray[T]] with { extension (self: HyArray[T]) { @@ -179,7 +178,7 @@ given hyArrayIsValue[T](using tIsValue: Value[T]): Value[HyArray[T]] with { } -given hyArrayIsCollection[T](using tIsValue: Value[T]): Collection[HyArray[T]] with { +given [T: Value] => Collection[HyArray[T]] with { type Element = T type Position = Int diff --git a/tests/pos/hylolib/Range.scala b/tests/pos/hylolib/Range.scala index 1f597652ead1..b0f50dd55c8c 100644 --- a/tests/pos/hylolib/Range.scala +++ b/tests/pos/hylolib/Range.scala @@ -30,7 +30,7 @@ object Range { * @requires * `lowerBound` is lesser than or equal to `upperBound`. */ - def apply[Bound](lowerBound: Bound, upperBound: Bound)(using Comparable[Bound]) = + def apply[Bound: Comparable](lowerBound: Bound, upperBound: Bound) = require(lowerBound `le` upperBound) new Range(lowerBound, upperBound) diff --git a/tests/pos/hylolib/Slice.scala b/tests/pos/hylolib/Slice.scala index 2289ac2a085b..b577ceeb3739 100644 --- a/tests/pos/hylolib/Slice.scala +++ b/tests/pos/hylolib/Slice.scala @@ -1,9 +1,7 @@ package hylo /** A view into a collection. */ -final class Slice[Base](using - val b: Collection[Base] -)( +final class Slice[Base: Collection as b]( val base: Base, val bounds: Range[b.Position] ) { @@ -26,7 +24,7 @@ final class Slice[Base](using } -given sliceIsCollection[T](using c: Collection[T]): Collection[Slice[T]] with { +given sliceIsCollection[T: Collection as c]: Collection[Slice[T]] with { type Element = c.Element type Position = c.Position From ab7a521bd7e196d0056885318e751af5811eaa09 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Jan 2024 23:10:11 +0100 Subject: [PATCH 21/63] Fix typing of generated context bound refinements Avoid accidental binding to self # Conflicts: # compiler/src/dotty/tools/dotc/typer/Typer.scala --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 3 ++- tests/pos/hylolib-extract.scala | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index e7466e822909..f066cd9ca2de 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2211,7 +2211,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer if tycon.tpe.typeParams.nonEmpty then typed(untpd.AppliedTypeTree(tyconSplice, tparam :: Nil)) else if Feature.enabled(modularity) && tycon.tpe.member(tpnme.Self).symbol.isAbstractType then - typed(untpd.RefinedTypeTree(tyconSplice, List(untpd.TypeDef(tpnme.Self, tparam)))) + val tparamSplice = untpd.TypedSplice(typedExpr(tparam)) + typed(untpd.RefinedTypeTree(tyconSplice, List(untpd.TypeDef(tpnme.Self, tparamSplice)))) else errorTree(tree, em"""Illegal context bound: ${tycon.tpe} does not take type parameters and diff --git a/tests/pos/hylolib-extract.scala b/tests/pos/hylolib-extract.scala index 3c16a0420a55..4349122d72cd 100644 --- a/tests/pos/hylolib-extract.scala +++ b/tests/pos/hylolib-extract.scala @@ -1,17 +1,25 @@ //> using options -language:experimental.modularity -source future package hylotest -trait Value[Self] +trait Value: + type Self + extension (self: Self) def eq(other: Self): Boolean /** A collection of elements accessible by their position. */ -trait Collection[Self]: +trait Collection: + type Self /** The type of the elements in the collection. */ type Element: Value class BitArray -given Value[Boolean] {} +given Boolean is Value: + extension (self: Self) def eq(other: Self): Boolean = + self == other -given Collection[BitArray] with +given BitArray is Collection: type Element = Boolean + +extension [Self: Value](self: Self) + def neq(other: Self): Boolean = !self.eq(other) From 16932711d33517bd13d86d3a3de068d5805eab14 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 2 Jan 2024 11:33:06 +0100 Subject: [PATCH 22/63] Hylolib: Switch to member based type classes --- tests/pos/hylolib/AnyCollection.scala | 4 ++-- tests/pos/hylolib/AnyValue.scala | 7 ++----- tests/pos/hylolib/BitArray.scala | 4 ++-- tests/pos/hylolib/Collection.scala | 13 +++++++------ tests/pos/hylolib/CoreTraits.scala | 11 +++++------ tests/pos/hylolib/HyArray.scala | 4 ++-- tests/pos/hylolib/Integers.scala | 6 +++--- tests/pos/hylolib/Slice.scala | 2 +- 8 files changed, 24 insertions(+), 27 deletions(-) diff --git a/tests/pos/hylolib/AnyCollection.scala b/tests/pos/hylolib/AnyCollection.scala index 50f4313e46ce..4c5b6af5a516 100644 --- a/tests/pos/hylolib/AnyCollection.scala +++ b/tests/pos/hylolib/AnyCollection.scala @@ -19,7 +19,7 @@ object AnyCollection { // and `anyValueIsValue` when the method is called on a collection of `Int`s. None of these // choices is even correct! Note also that the ambiguity is suppressed if the constructor of // `AnyValue` is declared with a context bound rather than an implicit parameter. - given Value[b.Position] = b.positionIsValue + given b.Position is Value = b.positionIsValue def start(): AnyValue = AnyValue(base.startPosition) @@ -42,7 +42,7 @@ object AnyCollection { } -given anyCollectionIsCollection[T: Value]: Collection[AnyCollection[T]] with { +given [T: Value] => AnyCollection[T] is Collection with { type Element = T type Position = AnyValue diff --git a/tests/pos/hylolib/AnyValue.scala b/tests/pos/hylolib/AnyValue.scala index 21f2965e102e..23ff6c3161cd 100644 --- a/tests/pos/hylolib/AnyValue.scala +++ b/tests/pos/hylolib/AnyValue.scala @@ -58,9 +58,9 @@ object AnyValue { } -given anyValueIsValue: Value[AnyValue] with { +given AnyValue is Value: - extension (self: AnyValue) { + extension (self: AnyValue) def copy(): AnyValue = self.copy() @@ -71,6 +71,3 @@ given anyValueIsValue: Value[AnyValue] with { def hashInto(hasher: Hasher): Hasher = self.hashInto(hasher) - } - -} diff --git a/tests/pos/hylolib/BitArray.scala b/tests/pos/hylolib/BitArray.scala index 3a0b4658f747..ae9f4d486ad6 100644 --- a/tests/pos/hylolib/BitArray.scala +++ b/tests/pos/hylolib/BitArray.scala @@ -318,7 +318,7 @@ object BitArray { } -given bitArrayPositionIsValue: Value[BitArray.Position] with { +given BitArray.Position is Value with { extension (self: BitArray.Position) { @@ -335,7 +335,7 @@ given bitArrayPositionIsValue: Value[BitArray.Position] with { } -given bitArrayIsCollection: Collection[BitArray] with { +given BitArray is Collection with { type Element = Boolean type Position = BitArray.Position diff --git a/tests/pos/hylolib/Collection.scala b/tests/pos/hylolib/Collection.scala index 2fc04f02b9ac..3adaf1ee7cb7 100644 --- a/tests/pos/hylolib/Collection.scala +++ b/tests/pos/hylolib/Collection.scala @@ -2,7 +2,8 @@ package hylo /** A collection of elements accessible by their position. */ -trait Collection[Self] { +trait Collection { + type Self /** The type of the elements in the collection. */ type Element: Value @@ -213,7 +214,7 @@ extension [Self: Collection as s](self: Self) { * @complexity * O(n) where n is the number of elements in `self`. */ - def minElement()(using Comparable[s.Element]): Option[s.Element] = + def minElement()(using s.Element is Comparable): Option[s.Element] = self.minElement(isLessThan = _ `lt` _) /** Returns the maximum element in `self`, using `isGreaterThan` to compare elements. @@ -229,7 +230,7 @@ extension [Self: Collection as s](self: Self) { * @complexity * O(n) where n is the number of elements in `self`. */ - def maxElement()(using Comparable[s.Element]): Option[s.Element] = + def maxElement()(using s.Element is Comparable): Option[s.Element] = self.maxElement(isGreaterThan = _ `gt` _) /** Returns the maximum element in `self`, using `isOrderedBefore` to compare elements. @@ -257,12 +258,12 @@ extension [Self: Collection as s](self: Self) { } -extension [Self: Collection as s](self: Self)(using - Value[s.Element] +extension [Self: Collection as s](self: Self)( + using s.Element is Value ) { /** Returns `true` if `self` contains the same elements as `other`, in the same order. */ - def elementsEqual[T](using o: Collection[T] { type Element = s.Element })(other: T): Boolean = + def elementsEqual[T](using o: T is Collection { type Element = s.Element })(other: T): Boolean = def loop(i: s.Position, j: o.Position): Boolean = if (i `eq` self.endPosition) { j `eq` other.endPosition diff --git a/tests/pos/hylolib/CoreTraits.scala b/tests/pos/hylolib/CoreTraits.scala index 01b2c5242af9..f4b3699b430e 100644 --- a/tests/pos/hylolib/CoreTraits.scala +++ b/tests/pos/hylolib/CoreTraits.scala @@ -5,7 +5,8 @@ package hylo * The data structure of and algorithms of Hylo's standard library operate "notional values" rather * than arbitrary references. This trait defines the basis operations of all values. */ -trait Value[Self] { +trait Value: + type Self extension (self: Self) { @@ -15,20 +16,18 @@ trait Value[Self] { /** Returns `true` iff `self` and `other` have an equivalent value. */ def eq(other: Self): Boolean + def neq(other: Self): Boolean = !self.eq(other) + /** Hashes the salient parts of `self` into `hasher`. */ def hashInto(hasher: Hasher): Hasher } -} - -extension [Self: Value](self: Self) def neq(other: Self): Boolean = !self.eq(other) - // ---------------------------------------------------------------------------- // Comparable // ---------------------------------------------------------------------------- -trait Comparable[Self] extends Value[Self] { +trait Comparable extends Value { extension (self: Self) { diff --git a/tests/pos/hylolib/HyArray.scala b/tests/pos/hylolib/HyArray.scala index 0fff45e744ec..6ce18df8945e 100644 --- a/tests/pos/hylolib/HyArray.scala +++ b/tests/pos/hylolib/HyArray.scala @@ -161,7 +161,7 @@ object HyArray { } -given [T: Value] => Value[HyArray[T]] with { +given [T: Value] => HyArray[T] is Value with { extension (self: HyArray[T]) { @@ -178,7 +178,7 @@ given [T: Value] => Value[HyArray[T]] with { } -given [T: Value] => Collection[HyArray[T]] with { +given [T: Value] => HyArray[T] is Collection with { type Element = T type Position = Int diff --git a/tests/pos/hylolib/Integers.scala b/tests/pos/hylolib/Integers.scala index b9bc203a88ea..cda607efca56 100644 --- a/tests/pos/hylolib/Integers.scala +++ b/tests/pos/hylolib/Integers.scala @@ -1,6 +1,6 @@ package hylo -given booleanIsValue: Value[Boolean] with { +given Boolean is Value with { extension (self: Boolean) { @@ -18,7 +18,7 @@ given booleanIsValue: Value[Boolean] with { } -given intIsValue: Value[Int] with { +given Int is Value with { extension (self: Int) { @@ -36,7 +36,7 @@ given intIsValue: Value[Int] with { } -given intIsComparable: Comparable[Int] with { +given Int is Comparable with { extension (self: Int) { diff --git a/tests/pos/hylolib/Slice.scala b/tests/pos/hylolib/Slice.scala index b577ceeb3739..a6fd5b938c82 100644 --- a/tests/pos/hylolib/Slice.scala +++ b/tests/pos/hylolib/Slice.scala @@ -24,7 +24,7 @@ final class Slice[Base: Collection as b]( } -given sliceIsCollection[T: Collection as c]: Collection[Slice[T]] with { +given [T: Collection as c] => Slice[T] is Collection with { type Element = c.Element type Position = c.Position From c997f357f7f6acfa6fe600e86e04cc5cabe97f86 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 2 Jan 2024 13:12:45 +0100 Subject: [PATCH 23/63] Hylolib: Use name of bound parameter for context bounds --- tests/pos/hylolib-extract.scala | 4 ++ tests/pos/hylolib/AnyCollection.scala | 2 +- tests/pos/hylolib/Collection.scala | 92 ++++++++++++--------------- 3 files changed, 44 insertions(+), 54 deletions(-) diff --git a/tests/pos/hylolib-extract.scala b/tests/pos/hylolib-extract.scala index 4349122d72cd..846e52f30df6 100644 --- a/tests/pos/hylolib-extract.scala +++ b/tests/pos/hylolib-extract.scala @@ -23,3 +23,7 @@ given BitArray is Collection: extension [Self: Value](self: Self) def neq(other: Self): Boolean = !self.eq(other) + +extension [Self: Collection](self: Self) + def elementsEqual[T: Collection { type Element = Self.Element } ](other: T): Boolean = + ??? diff --git a/tests/pos/hylolib/AnyCollection.scala b/tests/pos/hylolib/AnyCollection.scala index 4c5b6af5a516..8523daf88a01 100644 --- a/tests/pos/hylolib/AnyCollection.scala +++ b/tests/pos/hylolib/AnyCollection.scala @@ -19,7 +19,7 @@ object AnyCollection { // and `anyValueIsValue` when the method is called on a collection of `Int`s. None of these // choices is even correct! Note also that the ambiguity is suppressed if the constructor of // `AnyValue` is declared with a context bound rather than an implicit parameter. - given b.Position is Value = b.positionIsValue + given b.Position is Value = b.Position def start(): AnyValue = AnyValue(base.startPosition) diff --git a/tests/pos/hylolib/Collection.scala b/tests/pos/hylolib/Collection.scala index 3adaf1ee7cb7..ae5250a77ef3 100644 --- a/tests/pos/hylolib/Collection.scala +++ b/tests/pos/hylolib/Collection.scala @@ -2,16 +2,16 @@ package hylo /** A collection of elements accessible by their position. */ -trait Collection { +trait Collection: type Self /** The type of the elements in the collection. */ type Element: Value /** The type of a position in the collection. */ - type Position: Value as positionIsValue + type Position: Value - extension (self: Self) { + extension (self: Self) /** Returns `true` iff `self` is empty. */ def isEmpty: Boolean = @@ -70,27 +70,17 @@ trait Collection { */ def isBefore(i: Position, j: Position): Boolean = val e = self.endPosition - if (i.eq(e)) { - false - } else if (j.eq(e)) { - true - } else { - def _isBefore(n: Position): Boolean = - if (n.eq(j)) { - true - } else if (n.eq(e)) { - false - } else { - _isBefore(self.positionAfter(n)) - } - _isBefore(self.positionAfter(i)) - } - - } - -} - -extension [Self: Collection as s](self: Self) { + if i `eq` e then false + else if j `eq` e then true + else + def recur(n: Position): Boolean = + if n `eq` j then true + else if n `eq` e then false + else recur(self.positionAfter(n)) + recur(self.positionAfter(i)) +end Collection + +extension [Self: Collection](self: Self) { /** Returns the first element of `self` along with a slice containing the suffix after this * element, or `None` if `self` is empty. @@ -98,7 +88,7 @@ extension [Self: Collection as s](self: Self) { * @complexity * O(1) */ - def headAndTail: Option[(s.Element, Slice[Self])] = + def headAndTail: Option[(Self.Element, Slice[Self])] = if (self.isEmpty) { None } else { @@ -113,9 +103,9 @@ extension [Self: Collection as s](self: Self) { * @complexity * O(n) where n is the number of elements in `self`. */ - def reduce[T](partialResult: T, combine: (T, s.Element) => T): T = + def reduce[T](partialResult: T, combine: (T, Self.Element) => T): T = val e = self.endPosition - def loop(p: s.Position, r: T): T = + def loop(p: Self.Position, r: T): T = if (p.eq(e)) { r } else { @@ -132,9 +122,9 @@ extension [Self: Collection as s](self: Self) { * @complexity * O(n) where n is the number of elements in `self`. */ - def forEach(action: (s.Element) => Boolean): Boolean = + def forEach(action: (Self.Element) => Boolean): Boolean = val e = self.endPosition - def loop(p: s.Position): Boolean = + def loop(p: Self.Position): Boolean = if (p.eq(e)) { true } else if (!action(self.at(p))) { @@ -149,7 +139,7 @@ extension [Self: Collection as s](self: Self) { * @complexity * O(n) where n is the number of elements in `self`. */ - def map[T: Value](transform: (s.Element) => T): HyArray[T] = + def map[T: Value](transform: (Self.Element) => T): HyArray[T] = self.reduce( HyArray[T](), (r, e) => r.append(transform(e), assumeUniqueness = true) @@ -160,9 +150,9 @@ extension [Self: Collection as s](self: Self) { * @complexity * O(n) where n is the number of elements in `self`. */ - def filter(isIncluded: (s.Element) => Boolean): HyArray[s.Element] = + def filter(isIncluded: (Self.Element) => Boolean): HyArray[Self.Element] = self.reduce( - HyArray[s.Element](), + HyArray[Self.Element](), (r, e) => if (isIncluded(e)) then r.append(e, assumeUniqueness = true) else r ) @@ -171,7 +161,7 @@ extension [Self: Collection as s](self: Self) { * @complexity * O(n) where n is the number of elements in `self`. */ - def containsWhere(predicate: (s.Element) => Boolean): Boolean = + def containsWhere(predicate: (Self.Element) => Boolean): Boolean = self.firstPositionWhere(predicate) != None /** Returns `true` if all elements in `self` satisfy `predicate`. @@ -179,7 +169,7 @@ extension [Self: Collection as s](self: Self) { * @complexity * O(n) where n is the number of elements in `self`. */ - def allSatisfy(predicate: (s.Element) => Boolean): Boolean = + def allSatisfy(predicate: (Self.Element) => Boolean): Boolean = self.firstPositionWhere(predicate) == None /** Returns the position of the first element of `self` satisfying `predicate`, or `None` if no @@ -188,9 +178,9 @@ extension [Self: Collection as s](self: Self) { * @complexity * O(n) where n is the number of elements in `self`. */ - def firstPositionWhere(predicate: (s.Element) => Boolean): Option[s.Position] = + def firstPositionWhere(predicate: (Self.Element) => Boolean): Option[Self.Position] = val e = self.endPosition - def loop(p: s.Position): Option[s.Position] = + def loop(p: Self.Position): Option[Self.Position] = if (p.eq(e)) { None } else if (predicate(self.at(p))) { @@ -205,7 +195,7 @@ extension [Self: Collection as s](self: Self) { * @complexity * O(n) where n is the number of elements in `self`. */ - def minElement(isLessThan: (s.Element, s.Element) => Boolean): Option[s.Element] = + def minElement(isLessThan: (Self.Element, Self.Element) => Boolean): Option[Self.Element] = self.leastElement(isLessThan) // NOTE: I can't find a reasonable way to call this method. @@ -214,7 +204,7 @@ extension [Self: Collection as s](self: Self) { * @complexity * O(n) where n is the number of elements in `self`. */ - def minElement()(using s.Element is Comparable): Option[s.Element] = + def minElement()(using Self.Element is Comparable): Option[Self.Element] = self.minElement(isLessThan = _ `lt` _) /** Returns the maximum element in `self`, using `isGreaterThan` to compare elements. @@ -222,7 +212,7 @@ extension [Self: Collection as s](self: Self) { * @complexity * O(n) where n is the number of elements in `self`. */ - def maxElement(isGreaterThan: (s.Element, s.Element) => Boolean): Option[s.Element] = + def maxElement(isGreaterThan: (Self.Element, Self.Element) => Boolean): Option[Self.Element] = self.leastElement(isGreaterThan) /** Returns the maximum element in `self`. @@ -230,7 +220,7 @@ extension [Self: Collection as s](self: Self) { * @complexity * O(n) where n is the number of elements in `self`. */ - def maxElement()(using s.Element is Comparable): Option[s.Element] = + def maxElement()(using Self.Element is Comparable): Option[Self.Element] = self.maxElement(isGreaterThan = _ `gt` _) /** Returns the maximum element in `self`, using `isOrderedBefore` to compare elements. @@ -238,12 +228,12 @@ extension [Self: Collection as s](self: Self) { * @complexity * O(n) where n is the number of elements in `self`. */ - def leastElement(isOrderedBefore: (s.Element, s.Element) => Boolean): Option[s.Element] = + def leastElement(isOrderedBefore: (Self.Element, Self.Element) => Boolean): Option[Self.Element] = if (self.isEmpty) { None } else { val e = self.endPosition - def _least(p: s.Position, least: s.Element): s.Element = + def _least(p: Self.Position, least: Self.Element): Self.Element = if (p.eq(e)) { least } else { @@ -258,22 +248,18 @@ extension [Self: Collection as s](self: Self) { } -extension [Self: Collection as s](self: Self)( - using s.Element is Value -) { +extension [Self: Collection](self: Self) /** Returns `true` if `self` contains the same elements as `other`, in the same order. */ - def elementsEqual[T](using o: T is Collection { type Element = s.Element })(other: T): Boolean = - def loop(i: s.Position, j: o.Position): Boolean = - if (i `eq` self.endPosition) { + def elementsEqual[T: Collection { type Element = Self.Element } ](other: T): Boolean = + def loop(i: Self.Position, j: T.Position): Boolean = + if i `eq` self.endPosition then j `eq` other.endPosition - } else if (j `eq` other.endPosition) { + else if j `eq` other.endPosition then false - } else if (self.at(i) `neq` other.at(j)) { + else if self.at(i) `neq` other.at(j)then false - } else { + else loop(self.positionAfter(i), other.positionAfter(j)) - } loop(self.startPosition, other.startPosition) -} From 7175a21c59d50d86b2c2add9138813b81e86efbd Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 2 Jan 2024 17:13:48 +0100 Subject: [PATCH 24/63] Hylolib: Some stylistic tweaks --- tests/pos/hylolib/Collection.scala | 89 +++++++++++------------------- tests/pos/hylolib/HyArray.scala | 2 +- 2 files changed, 34 insertions(+), 57 deletions(-) diff --git a/tests/pos/hylolib/Collection.scala b/tests/pos/hylolib/Collection.scala index ae5250a77ef3..ccc9e317aaf9 100644 --- a/tests/pos/hylolib/Collection.scala +++ b/tests/pos/hylolib/Collection.scala @@ -24,9 +24,9 @@ trait Collection: */ def count: Int = val e = endPosition - def _count(p: Position, n: Int): Int = - if p `eq` e then n else _count(self.positionAfter(p), n + 1) - _count(startPosition, 0) + def loop(p: Position, n: Int): Int = + if p `eq` e then n else loop(self.positionAfter(p), n + 1) + loop(startPosition, 0) /** Returns the position of `self`'s first element', or `endPosition` if `self` is empty. * @@ -80,7 +80,7 @@ trait Collection: recur(self.positionAfter(i)) end Collection -extension [Self: Collection](self: Self) { +extension [Self: Collection](self: Self) /** Returns the first element of `self` along with a slice containing the suffix after this * element, or `None` if `self` is empty. @@ -89,28 +89,24 @@ extension [Self: Collection](self: Self) { * O(1) */ def headAndTail: Option[(Self.Element, Slice[Self])] = - if (self.isEmpty) { + if self.isEmpty then None - } else { + else val p = self.startPosition val q = self.positionAfter(p) val t = Slice(self, Range(q, self.endPosition, (a, b) => (a `eq` b) || self.isBefore(a, b))) Some((self.at(p), t)) - } /** Applies `combine` on `partialResult` and each element of `self`, in order. * * @complexity * O(n) where n is the number of elements in `self`. */ - def reduce[T](partialResult: T, combine: (T, Self.Element) => T): T = + def reduce[T](partialResult: T)(combine: (T, Self.Element) => T): T = val e = self.endPosition def loop(p: Self.Position, r: T): T = - if (p.eq(e)) { - r - } else { - loop(self.positionAfter(p), combine(r, self.at(p))) - } + if p `eq` e then r + else loop(self.positionAfter(p), combine(r, self.at(p))) loop(self.startPosition, partialResult) /** Applies `action` on each element of `self`, in order, until `action` returns `false`, and @@ -122,16 +118,12 @@ extension [Self: Collection](self: Self) { * @complexity * O(n) where n is the number of elements in `self`. */ - def forEach(action: (Self.Element) => Boolean): Boolean = + def forEach(action: Self.Element => Boolean): Boolean = val e = self.endPosition def loop(p: Self.Position): Boolean = - if (p.eq(e)) { - true - } else if (!action(self.at(p))) { - false - } else { - loop(self.positionAfter(p)) - } + if p `eq` e then true + else if !action(self.at(p)) then false + else loop(self.positionAfter(p)) loop(self.startPosition) /** Returns a collection with the elements of `self` transformed by `transform`, in order. @@ -139,29 +131,25 @@ extension [Self: Collection](self: Self) { * @complexity * O(n) where n is the number of elements in `self`. */ - def map[T: Value](transform: (Self.Element) => T): HyArray[T] = - self.reduce( - HyArray[T](), - (r, e) => r.append(transform(e), assumeUniqueness = true) - ) + def map[T: Value](transform: Self.Element => T): HyArray[T] = + self.reduce(HyArray[T]()): (r, e) => + r.append(transform(e), assumeUniqueness = true) /** Returns a collection with the elements of `self` satisfying `isInclude`, in order. * * @complexity * O(n) where n is the number of elements in `self`. */ - def filter(isIncluded: (Self.Element) => Boolean): HyArray[Self.Element] = - self.reduce( - HyArray[Self.Element](), - (r, e) => if (isIncluded(e)) then r.append(e, assumeUniqueness = true) else r - ) + def filter(isIncluded: Self.Element => Boolean): HyArray[Self.Element] = + self.reduce(HyArray[Self.Element]()): (r, e) => + if isIncluded(e) then r.append(e, assumeUniqueness = true) else r /** Returns `true` if `self` contains an element satisfying `predicate`. * * @complexity * O(n) where n is the number of elements in `self`. */ - def containsWhere(predicate: (Self.Element) => Boolean): Boolean = + def containsWhere(predicate: Self.Element => Boolean): Boolean = self.firstPositionWhere(predicate) != None /** Returns `true` if all elements in `self` satisfy `predicate`. @@ -169,7 +157,7 @@ extension [Self: Collection](self: Self) { * @complexity * O(n) where n is the number of elements in `self`. */ - def allSatisfy(predicate: (Self.Element) => Boolean): Boolean = + def allSatisfy(predicate: Self.Element => Boolean): Boolean = self.firstPositionWhere(predicate) == None /** Returns the position of the first element of `self` satisfying `predicate`, or `None` if no @@ -178,16 +166,12 @@ extension [Self: Collection](self: Self) { * @complexity * O(n) where n is the number of elements in `self`. */ - def firstPositionWhere(predicate: (Self.Element) => Boolean): Option[Self.Position] = + def firstPositionWhere(predicate: Self.Element => Boolean): Option[Self.Position] = val e = self.endPosition def loop(p: Self.Position): Option[Self.Position] = - if (p.eq(e)) { - None - } else if (predicate(self.at(p))) { - Some(p) - } else { - loop(self.positionAfter(p)) - } + if p `eq` e then None + else if predicate(self.at(p)) then Some(p) + else loop(self.positionAfter(p)) loop(self.startPosition) /** Returns the minimum element in `self`, using `isLessThan` to compare elements. @@ -229,26 +213,19 @@ extension [Self: Collection](self: Self) { * O(n) where n is the number of elements in `self`. */ def leastElement(isOrderedBefore: (Self.Element, Self.Element) => Boolean): Option[Self.Element] = - if (self.isEmpty) { + if self.isEmpty then None - } else { + else val e = self.endPosition - def _least(p: Self.Position, least: Self.Element): Self.Element = - if (p.eq(e)) { + def loop(p: Self.Position, least: Self.Element): Self.Element = + if p `eq` e then least - } else { + else val x = self.at(p) val y = if isOrderedBefore(x, least) then x else least - _least(self.positionAfter(p), y) - } - + loop(self.positionAfter(p), y) val b = self.startPosition - Some(_least(self.positionAfter(b), self.at(b))) - } - -} - -extension [Self: Collection](self: Self) + Some(loop(self.positionAfter(b), self.at(b))) /** Returns `true` if `self` contains the same elements as `other`, in the same order. */ def elementsEqual[T: Collection { type Element = Self.Element } ](other: T): Boolean = @@ -262,4 +239,4 @@ extension [Self: Collection](self: Self) else loop(self.positionAfter(i), other.positionAfter(j)) loop(self.startPosition, other.startPosition) - +end extension diff --git a/tests/pos/hylolib/HyArray.scala b/tests/pos/hylolib/HyArray.scala index 6ce18df8945e..f855d429a57a 100644 --- a/tests/pos/hylolib/HyArray.scala +++ b/tests/pos/hylolib/HyArray.scala @@ -172,7 +172,7 @@ given [T: Value] => HyArray[T] is Value with { self.elementsEqual(other) def hashInto(hasher: Hasher): Hasher = - self.reduce(hasher, (h, e) => e.hashInto(h)) + self.reduce(hasher)((h, e) => e.hashInto(h)) } From ae6fb3cdc8588a061ec5f5c6a4b4929d63e1e27d Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 2 Jan 2024 18:30:28 +0100 Subject: [PATCH 25/63] Hylolib: Drop usages of `with` in givens --- tests/pos/hylolib/AnyCollection.scala | 7 +-- tests/pos/hylolib/BitArray.scala | 20 +++------ tests/pos/hylolib/HyArray.scala | 55 ++++++++--------------- tests/pos/hylolib/Integers.scala | 26 +++-------- tests/pos/hylolib/Slice.scala | 22 ++++----- tests/pos/hylolib/StringConvertible.scala | 15 +++---- 6 files changed, 47 insertions(+), 98 deletions(-) diff --git a/tests/pos/hylolib/AnyCollection.scala b/tests/pos/hylolib/AnyCollection.scala index 8523daf88a01..dc42b5651514 100644 --- a/tests/pos/hylolib/AnyCollection.scala +++ b/tests/pos/hylolib/AnyCollection.scala @@ -42,12 +42,12 @@ object AnyCollection { } -given [T: Value] => AnyCollection[T] is Collection with { +given [T: Value] => AnyCollection[T] is Collection: type Element = T type Position = AnyValue - extension (self: AnyCollection[T]) { + extension (self: AnyCollection[T]) def startPosition = self._start() @@ -61,6 +61,3 @@ given [T: Value] => AnyCollection[T] is Collection with { def at(p: Position) = self._at(p) - } - -} diff --git a/tests/pos/hylolib/BitArray.scala b/tests/pos/hylolib/BitArray.scala index ae9f4d486ad6..6ef406e5ad83 100644 --- a/tests/pos/hylolib/BitArray.scala +++ b/tests/pos/hylolib/BitArray.scala @@ -318,9 +318,9 @@ object BitArray { } -given BitArray.Position is Value with { +given BitArray.Position is Value: - extension (self: BitArray.Position) { + extension (self: BitArray.Position) def copy(): BitArray.Position = self.copy() @@ -331,16 +331,12 @@ given BitArray.Position is Value with { def hashInto(hasher: Hasher): Hasher = self.hashInto(hasher) - } - -} - -given BitArray is Collection with { +given BitArray is Collection: type Element = Boolean type Position = BitArray.Position - extension (self: BitArray) { + extension (self: BitArray) override def count: Int = self.count @@ -357,16 +353,10 @@ given BitArray is Collection with { def at(p: BitArray.Position): Boolean = self.at(p) - } - -} - -given bitArrayIsStringConvertible: StringConvertible[BitArray] with { - +given BitArray is StringConvertible: extension (self: BitArray) override def description: String = var contents = mutable.StringBuilder() self.forEach((e) => { contents += (if e then '1' else '0'); true }) contents.mkString -} diff --git a/tests/pos/hylolib/HyArray.scala b/tests/pos/hylolib/HyArray.scala index f855d429a57a..ee2077089b2e 100644 --- a/tests/pos/hylolib/HyArray.scala +++ b/tests/pos/hylolib/HyArray.scala @@ -57,15 +57,13 @@ final class HyArray[Element: Value as elementIsCValue]( result // NOTE: Can't refine `C.Element` without renaming the generic parameter of `HyArray`. - // /** Adds the contents of `source` at the end of the array. */ - // def appendContents[C](using - // s: Collection[C] - // )( - // source: C { type Element = Element }, - // assumeUniqueness: Boolean = false - // ): HyArray[Element] = - // val result = if (assumeUniqueness) { this } else { copy(count + source.count) } - // source.reduce(result, (r, e) => r.append(e, assumeUniqueness = true)) + /** Adds the contents of `source` at the end of the array. */ + def appendContents[C: Collection { type Element = HyArray.this.Element }]( + source: C, assumeUniqueness: Boolean = false + ): HyArray[Element] = + val result = if (assumeUniqueness) { this } else { copy(count + source.count) } + source.reduce(result): (r, e) => + r.append(e, assumeUniqueness = true) /** Removes and returns the last element, or returns `None` if the array is empty. */ def popLast(assumeUniqueness: Boolean = false): (HyArray[Element], Option[Element]) = @@ -161,9 +159,9 @@ object HyArray { } -given [T: Value] => HyArray[T] is Value with { +given [T: Value] => HyArray[T] is Value: - extension (self: HyArray[T]) { + extension (self: HyArray[T]) def copy(): HyArray[T] = self.copy() @@ -174,16 +172,12 @@ given [T: Value] => HyArray[T] is Value with { def hashInto(hasher: Hasher): Hasher = self.reduce(hasher)((h, e) => e.hashInto(h)) - } - -} - -given [T: Value] => HyArray[T] is Collection with { +given [T: Value] => HyArray[T] is Collection: type Element = T type Position = Int - extension (self: HyArray[T]) { + extension (self: HyArray[T]) // NOTE: Having to explicitly override means that primary declaration can't automatically // specialize trait requirements. @@ -199,22 +193,11 @@ given [T: Value] => HyArray[T] is Collection with { def at(p: Int) = self.at(p) - } - -} - -// NOTE: This should work. -// given hyArrayIsStringConvertible[T](using -// tIsValue: Value[T], -// tIsStringConvertible: StringConvertible[T] -// ): StringConvertible[HyArray[T]] with { -// -// given Collection[HyArray[T]] = hyArrayIsCollection[T] -// -// extension (self: HyArray[T]) -// override def description: String = -// var contents = mutable.StringBuilder() -// self.forEach((e) => { contents ++= e.description; true }) -// s"[${contents.mkString(", ")}]" -// -// } +given [T: {Value, StringConvertible}] => HyArray[T] is StringConvertible: + extension (self: HyArray[T]) + override def description: String = + val contents = mutable.StringBuilder() + self.forEach: e => + contents ++= e.description + true + s"[${contents.mkString(", ")}]" diff --git a/tests/pos/hylolib/Integers.scala b/tests/pos/hylolib/Integers.scala index cda607efca56..313ae7cfcd21 100644 --- a/tests/pos/hylolib/Integers.scala +++ b/tests/pos/hylolib/Integers.scala @@ -1,8 +1,8 @@ package hylo -given Boolean is Value with { +given Boolean is Value: - extension (self: Boolean) { + extension (self: Boolean) def copy(): Boolean = // Note: Scala's `Boolean` has value semantics already. @@ -14,13 +14,9 @@ given Boolean is Value with { def hashInto(hasher: Hasher): Hasher = hasher.combine(if self then 1 else 0) - } +given Int is Value: -} - -given Int is Value with { - - extension (self: Int) { + extension (self: Int) def copy(): Int = // Note: Scala's `Int` has value semantics already. @@ -32,13 +28,9 @@ given Int is Value with { def hashInto(hasher: Hasher): Hasher = hasher.combine(self) - } - -} +given Int is Comparable: -given Int is Comparable with { - - extension (self: Int) { + extension (self: Int) def copy(): Int = self @@ -51,8 +43,4 @@ given Int is Comparable with { def lt(other: Int): Boolean = self < other - } - -} - -given intIsStringConvertible: StringConvertible[Int] with {} +given Int is StringConvertible {} diff --git a/tests/pos/hylolib/Slice.scala b/tests/pos/hylolib/Slice.scala index a6fd5b938c82..227ed8a46a1f 100644 --- a/tests/pos/hylolib/Slice.scala +++ b/tests/pos/hylolib/Slice.scala @@ -1,35 +1,35 @@ package hylo /** A view into a collection. */ -final class Slice[Base: Collection as b]( - val base: Base, - val bounds: Range[b.Position] +final class Slice[Base: Collection]( + tracked val base: Base, + tracked val bounds: Range[Base.Position] ) { /** Returns `true` iff `this` is empty. */ def isEmpty: Boolean = bounds.lowerBound.eq(bounds.upperBound) - def startPosition: b.Position = + def startPosition: Base.Position = bounds.lowerBound - def endPosition: b.Position = + def endPosition: Base.Position = bounds.upperBound - def positionAfter(p: b.Position): b.Position = + def positionAfter(p: Base.Position): Base.Position = base.positionAfter(p) - def at(p: b.Position): b.Element = + def at(p: Base.Position): Base.Element = base.at(p) } -given [T: Collection as c] => Slice[T] is Collection with { +given [T: Collection as c] => Slice[T] is Collection: type Element = c.Element type Position = c.Position - extension (self: Slice[T]) { + extension (self: Slice[T]) def startPosition = self.bounds.lowerBound.asInstanceOf[Position] // NOTE: Ugly hack @@ -38,7 +38,3 @@ given [T: Collection as c] => Slice[T] is Collection with { def positionAfter(p: Position) = self.base.positionAfter(p) def at(p: Position) = self.base.at(p) - - } - -} diff --git a/tests/pos/hylolib/StringConvertible.scala b/tests/pos/hylolib/StringConvertible.scala index 0702f79f2794..cf901d9a3313 100644 --- a/tests/pos/hylolib/StringConvertible.scala +++ b/tests/pos/hylolib/StringConvertible.scala @@ -1,14 +1,9 @@ package hylo /** A type whose instances can be described by a character string. */ -trait StringConvertible[Self] { +trait StringConvertible: + type Self - extension (self: Self) { - - /** Returns a textual description of `self`. */ - def description: String = - self.toString - - } - -} + /** Returns a textual description of `self`. */ + extension (self: Self) + def description: String = self.toString From d5433c5dcdd3f1dcfa0884839b78b4c739278314 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 3 Jan 2024 18:34:31 +0100 Subject: [PATCH 26/63] Dealias before checking for illegal class parents --- compiler/src/dotty/tools/dotc/transform/PostTyper.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 18222dd2f66b..5c86591280f7 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -424,7 +424,7 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => // these refinements are split off from the parent type constructor // application `parent` in Namer and don't show up as parent types // of the class. - val illegalRefs = parent.tpe.stripRefinement.namedPartsWith: + val illegalRefs = parent.tpe.dealias.stripRefinement.namedPartsWith: p => p.symbol.is(ParamAccessor) && (p.symbol.owner eq sym) if illegalRefs.nonEmpty then report.error( From 7b51c58b717253f7ae1c0a09b575ecaf10517d99 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 3 Jan 2024 18:35:02 +0100 Subject: [PATCH 27/63] Hylolib: Alternative, statically safe Slice design --- tests/pos/hylolib/AnyCollection.scala | 16 +++--------- tests/pos/hylolib/AnyValue.scala | 12 +++------ tests/pos/hylolib/Collection.scala | 25 ++++++++++++++++++ tests/pos/hylolib/Slice.scala | 37 ++++++++++++++++++++++----- 4 files changed, 62 insertions(+), 28 deletions(-) diff --git a/tests/pos/hylolib/AnyCollection.scala b/tests/pos/hylolib/AnyCollection.scala index dc42b5651514..cec8b95ee077 100644 --- a/tests/pos/hylolib/AnyCollection.scala +++ b/tests/pos/hylolib/AnyCollection.scala @@ -48,16 +48,8 @@ given [T: Value] => AnyCollection[T] is Collection: type Position = AnyValue extension (self: AnyCollection[T]) - - def startPosition = - self._start() - - def endPosition = - self._end() - - def positionAfter(p: Position) = - self._after(p) - - def at(p: Position) = - self._at(p) + def startPosition = self._start() + def endPosition = self._end() + def positionAfter(p: Position) = self._after(p) + def at(p: Position) = self._at(p) diff --git a/tests/pos/hylolib/AnyValue.scala b/tests/pos/hylolib/AnyValue.scala index 23ff6c3161cd..6844135b646b 100644 --- a/tests/pos/hylolib/AnyValue.scala +++ b/tests/pos/hylolib/AnyValue.scala @@ -61,13 +61,7 @@ object AnyValue { given AnyValue is Value: extension (self: AnyValue) - - def copy(): AnyValue = - self.copy() - - def eq(other: AnyValue): Boolean = - self `eq` other - - def hashInto(hasher: Hasher): Hasher = - self.hashInto(hasher) + def copy(): AnyValue = self.copy() + def eq(other: AnyValue): Boolean = self `eq` other + def hashInto(hasher: Hasher): Hasher = self.hashInto(hasher) diff --git a/tests/pos/hylolib/Collection.scala b/tests/pos/hylolib/Collection.scala index ccc9e317aaf9..bef86a967e6e 100644 --- a/tests/pos/hylolib/Collection.scala +++ b/tests/pos/hylolib/Collection.scala @@ -78,6 +78,22 @@ trait Collection: else if n `eq` e then false else recur(self.positionAfter(n)) recur(self.positionAfter(i)) + + class Slice2(val base: Self, val bounds: Range[Position]): + + def isEmpty: Boolean = + bounds.lowerBound.eq(bounds.upperBound) + + def startPosition: Position = + bounds.lowerBound + + def endPosition: Position = + bounds.upperBound + + def at(p: Position): Element = + base.at(p) + end Slice2 + end Collection extension [Self: Collection](self: Self) @@ -97,6 +113,15 @@ extension [Self: Collection](self: Self) val t = Slice(self, Range(q, self.endPosition, (a, b) => (a `eq` b) || self.isBefore(a, b))) Some((self.at(p), t)) + def headAndTail2: Option[(Self.Element, Self.Slice2)] = + if self.isEmpty then + None + else + val p = self.startPosition + val q = self.positionAfter(p) + val t = Self.Slice2(self, Range(q, self.endPosition, (a, b) => (a `eq` b) || self.isBefore(a, b))) + Some((self.at(p), t)) + /** Applies `combine` on `partialResult` and each element of `self`, in order. * * @complexity diff --git a/tests/pos/hylolib/Slice.scala b/tests/pos/hylolib/Slice.scala index 227ed8a46a1f..d54f855b1041 100644 --- a/tests/pos/hylolib/Slice.scala +++ b/tests/pos/hylolib/Slice.scala @@ -2,8 +2,8 @@ package hylo /** A view into a collection. */ final class Slice[Base: Collection]( - tracked val base: Base, - tracked val bounds: Range[Base.Position] + val base: Base, + val bounds: Range[Base.Position] ) { /** Returns `true` iff `this` is empty. */ @@ -24,17 +24,40 @@ final class Slice[Base: Collection]( } -given [T: Collection as c] => Slice[T] is Collection: +given [C: Collection] => Slice[C] is Collection: - type Element = c.Element - type Position = c.Position + type Element = C.Element + type Position = C.Position - extension (self: Slice[T]) + extension (self: Slice[C]) - def startPosition = self.bounds.lowerBound.asInstanceOf[Position] // NOTE: Ugly hack + def startPosition = self.bounds.lowerBound.asInstanceOf[Position] + // This is actually unsafe. We have: + // self.bounds: Range(Slice[C].Base.Position) + // But the _value_ of Slice[C].Base is not necssarily this given, even + // though it is true that `type Slice[C].Base = C`. There might be multiple + // implementations of `Slice[C] is Collection` that define different `Position` + // types. So we cannot conclude that `Slice[C].Base.Position = this.Position`. + // To make this safe, we'd need some form of coherence, where we ensure that + // there is only one way to implement `Slice is Collection`. + // + // As an alternativem we can make Slice dependent on the original Collection + // _instance_ instead of the original Collection _type_. This design is + // realized by the Slice2 definitions. It works without casts. def endPosition = self.bounds.upperBound.asInstanceOf[Position] def positionAfter(p: Position) = self.base.positionAfter(p) def at(p: Position) = self.base.at(p) + +given [C: Collection] => C.Slice2 is Collection: + type Element = C.Element + type Position = C.Position + + extension (self: C.Slice2) + + def startPosition = self.bounds.lowerBound + def endPosition = self.bounds.upperBound + def positionAfter(p: Position) = self.base.positionAfter(p) + def at(p: Position) = self.base.at(p) From 62d08e353a7ff78e0f51bd49a18beae5cf88cfed Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Jan 2024 23:27:07 +0100 Subject: [PATCH 28/63] Rename `deferredSummon` to `deferred` and allow it for defs The idea is that `given ... = deferred` should be the new syntax for abstract givens. These can only be defined in traits and need to be implemented in the first subclass extending the trait. If the definition of a given vals is missing, one is synthesized by an implicit search at the site of the extending class. This will free the syntax given A is TC for the more common case where we just want to implement a marker type class TC. Also allow # Conflicts: # compiler/src/dotty/tools/dotc/typer/Namer.scala --- .../src/dotty/tools/dotc/core/StdNames.scala | 2 +- .../dotty/tools/dotc/typer/Implicits.scala | 4 +-- .../src/dotty/tools/dotc/typer/Namer.scala | 8 ++--- .../src/dotty/tools/dotc/typer/Typer.scala | 29 +++++++++++++++---- tests/neg/deferred-givens.check | 13 +++++++++ tests/neg/deferred-givens.scala | 28 ++++++++++++++++++ tests/neg/deferredSummon.scala | 6 ++-- tests/pos/deferredSummon.scala | 2 +- 8 files changed, 75 insertions(+), 17 deletions(-) create mode 100644 tests/neg/deferred-givens.check create mode 100644 tests/neg/deferred-givens.scala diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 42103922b2ec..dde7f7149055 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -453,7 +453,7 @@ object StdNames { val create: N = "create" val currentMirror: N = "currentMirror" val curried: N = "curried" - val deferredSummon: N = "deferredSummon" + val deferred: N = "deferred" val definitions: N = "definitions" val delayedInit: N = "delayedInit" val delayedInitArg: N = "delayedInit$body" diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 5162b3fed1b9..c4ab7381bf5f 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -922,10 +922,10 @@ trait Implicits: /** Search an implicit argument and report error if not found */ - def implicitArgTree(formal: Type, span: Span)(using Context): Tree = { + def implicitArgTree(formal: Type, span: Span, where: => String = "")(using Context): Tree = { val arg = inferImplicitArg(formal, span) if (arg.tpe.isInstanceOf[SearchFailureType]) - report.error(missingArgMsg(arg, formal, ""), ctx.source.atSpan(span)) + report.error(missingArgMsg(arg, formal, where), ctx.source.atSpan(span)) arg } diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 8115cf7483c2..09324e9d42b6 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -1806,12 +1806,12 @@ class Namer { typer: Typer => WildcardType } - // translate `given T = deferredSummon` to an abstract given with HasDefault flag - if sym.is(Given, butNot = Method) then + // translate `given T = deferred` to an abstract given with HasDefault flag + if sym.is(Given) then mdef.rhs match - case Ident(nme.deferredSummon) if Feature.enabled(modularity) => + case Ident(nme.deferred) if Feature.enabled(modularity) => if !sym.maybeOwner.is(Trait) then - report.error(em"`deferredSummon` can only be used for givens in traits", mdef.rhs.srcPos) + report.error(em"`deferred` can only be used for givens in traits", mdef.rhs.srcPos) else sym.resetFlag(Final | Lazy) sym.setFlag(Deferred | HasDefault) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index f066cd9ca2de..ffc5ed1fb0ba 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2579,7 +2579,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val rhs1 = vdef.rhs match { case rhs @ Ident(nme.WILDCARD) => rhs.withType(tpt1.tpe) - case Ident(nme.deferredSummon) if sym.isAllOf(DeferredGivenFlags, butNot = Param) => + case Ident(nme.deferred) if sym.isAllOf(DeferredGivenFlags, butNot = Param) => EmptyTree case rhs => typedExpr(rhs, tpt1.tpe.widenExpr) @@ -2643,9 +2643,13 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer if sym.isInlineMethod then rhsCtx.addMode(Mode.InlineableBody) if sym.is(ExtensionMethod) then rhsCtx.addMode(Mode.InExtensionMethod) - val rhs1 = PrepareInlineable.dropInlineIfError(sym, - if sym.isScala2Macro then typedScala2MacroBody(ddef.rhs)(using rhsCtx) - else typedExpr(ddef.rhs, tpt1.tpe.widenExpr)(using rhsCtx)) + val rhs1 = ddef.rhs match + case Ident(nme.deferred) if sym.isAllOf(DeferredGivenFlags) => + EmptyTree + case rhs => + PrepareInlineable.dropInlineIfError(sym, + if sym.isScala2Macro then typedScala2MacroBody(ddef.rhs)(using rhsCtx) + else typedExpr(ddef.rhs, tpt1.tpe.widenExpr)(using rhsCtx)) if sym.isInlineMethod then if StagingLevel.level > 0 then @@ -2826,7 +2830,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case None => body - /** Implement givens that were declared with a `deferredSummon` rhs. + /** Implement givens that were declared with a `deferred` rhs. * The a given value matching the declared type is searched in a * context directly enclosing the current class, in which all given * parameters of the current class are also defined. @@ -2834,13 +2838,25 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def implementDeferredGivens(body: List[Tree]): List[Tree] = if cls.is(Trait) then body else + def isGivenValue(mbr: TermRef) = + val dcl = mbr.symbol + if dcl.is(Method) then + report.error( + em"""Cannnot infer the implementation of the deferred ${dcl.showLocated} + |since that given is parameterized. An implementing given needs to be written explicitly.""", + cdef.srcPos) + false + else true + def givenImpl(mbr: TermRef): ValDef = val dcl = mbr.symbol val target = dcl.info.asSeenFrom(cls.thisType, dcl.owner) val constr = cls.primaryConstructor val paramScope = newScopeWith(cls.paramAccessors.filter(_.is(Given))*) val searchCtx = ctx.outer.fresh.setScope(paramScope) - val rhs = implicitArgTree(target, cdef.span)(using searchCtx) + val rhs = implicitArgTree(target, cdef.span, + where = i"inferring the implementation of the deferred ${dcl.showLocated}" + )(using searchCtx) val impl = dcl.copy(cls, flags = dcl.flags &~ (HasDefault | Deferred) | Final, info = target, @@ -2850,6 +2866,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val givenImpls = cls.thisType.implicitMembers .filter(_.symbol.isAllOf(DeferredGivenFlags, butNot = Param)) + .filter(isGivenValue) .map(givenImpl) body ++ givenImpls end implementDeferredGivens diff --git a/tests/neg/deferred-givens.check b/tests/neg/deferred-givens.check new file mode 100644 index 000000000000..771dd98f40c8 --- /dev/null +++ b/tests/neg/deferred-givens.check @@ -0,0 +1,13 @@ +-- [E172] Type Error: tests/neg/deferred-givens.scala:9:6 -------------------------------------------------------------- +9 |class B extends A // error + |^^^^^^^^^^^^^^^^^ + |No given instance of type Ctx was found for inferring the implementation of the deferred given instance ctx in trait A +-- [E172] Type Error: tests/neg/deferred-givens.scala:11:15 ------------------------------------------------------------ +11 |abstract class C extends A // error + |^^^^^^^^^^^^^^^^^^^^^^^^^^ + |No given instance of type Ctx was found for inferring the implementation of the deferred given instance ctx in trait A +-- Error: tests/neg/deferred-givens.scala:24:8 ------------------------------------------------------------------------- +24 | class E extends A2 // error, can't summon polymorphic given + | ^^^^^^^^^^^^^^^^^^ + | Cannnot infer the implementation of the deferred given instance given_Ctx3_T in trait A2 + | since that given is parameterized. An implementing given needs to be written explicitly. diff --git a/tests/neg/deferred-givens.scala b/tests/neg/deferred-givens.scala new file mode 100644 index 000000000000..08dcdbf7ae52 --- /dev/null +++ b/tests/neg/deferred-givens.scala @@ -0,0 +1,28 @@ +//> using options -YXtypeclass -source future +class Ctx +class Ctx2 + +trait A: + given Ctx as ctx = deferred + given Ctx2 = deferred + +class B extends A // error + +abstract class C extends A // error + +class D extends A: + given Ctx as ctx = Ctx() // ok, was implemented + given Ctx2 = Ctx2() // ok + +class Ctx3[T] + +trait A2: + given [T] => Ctx3[T] = deferred + +object O: + given [T] => Ctx3[T] = Ctx3[T]() + class E extends A2 // error, can't summon polymorphic given + +class E extends A2: + given [T] => Ctx3[T] = Ctx3[T]() // ok + diff --git a/tests/neg/deferredSummon.scala b/tests/neg/deferredSummon.scala index c4c9e36fce34..e379bee27f7a 100644 --- a/tests/neg/deferredSummon.scala +++ b/tests/neg/deferredSummon.scala @@ -1,12 +1,12 @@ //> using options -language:experimental.modularity object Test: - given Int = deferredSummon // error + given Int = deferred // error abstract class C: - given Int = deferredSummon // error + given Int = deferred // error trait A: locally: - given Int = deferredSummon // error + given Int = deferred // error diff --git a/tests/pos/deferredSummon.scala b/tests/pos/deferredSummon.scala index f9e8953cee6e..01a034d3cdba 100644 --- a/tests/pos/deferredSummon.scala +++ b/tests/pos/deferredSummon.scala @@ -5,7 +5,7 @@ trait Ord: trait A: type Elem - given Elem is Ord = deferredSummon + given Elem is Ord = deferred def foo = summon[Elem is Ord] trait B: From 2cd7028ad57c0ef0c088eac2aea398afdb47468d Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Jan 2024 23:27:59 +0100 Subject: [PATCH 29/63] Treat new style given syntax without a rhs or template as concrete To make an abstract given with new style syntax, one needs to write `= deferred`. --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 2 +- tests/neg/deferred-givens.scala | 2 +- tests/pos/hylolib/Integers.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 77cba5b6f1d0..d228dc456af2 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -4159,7 +4159,7 @@ object Parsers { ValDef(name, parents.head, subExpr()) else DefDef(name, adjustDefParams(joinParams(tparams, vparamss)), parents.head, subExpr()) - else if (isStatSep || isStatSeqEnd) && parentsIsType then + else if (isStatSep || isStatSeqEnd) && parentsIsType && !newSyntaxAllowed then if name.isEmpty then syntaxError(em"anonymous given cannot be abstract") DefDef(name, adjustDefParams(joinParams(tparams, vparamss)), parents.head, EmptyTree) diff --git a/tests/neg/deferred-givens.scala b/tests/neg/deferred-givens.scala index 08dcdbf7ae52..8581f052c663 100644 --- a/tests/neg/deferred-givens.scala +++ b/tests/neg/deferred-givens.scala @@ -1,4 +1,4 @@ -//> using options -YXtypeclass -source future +//> using options -language:experimental.modularity -source future class Ctx class Ctx2 diff --git a/tests/pos/hylolib/Integers.scala b/tests/pos/hylolib/Integers.scala index 313ae7cfcd21..f7334ae40786 100644 --- a/tests/pos/hylolib/Integers.scala +++ b/tests/pos/hylolib/Integers.scala @@ -43,4 +43,4 @@ given Int is Comparable: def lt(other: Int): Boolean = self < other -given Int is StringConvertible {} +given Int is StringConvertible From c7be3131fc1e39f3c4b5be38de74dd78114a4a3e Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Jan 2024 23:44:48 +0100 Subject: [PATCH 30/63] Fix typing of RefinedTypes with watching parents If a refined type has a parent type watching some other type, the parent should not be mapped to Object. Previously, the parent counted as `isEmpty` which caused this mapping. Also: Complete and cleanup hylolib tests --- .../src/dotty/tools/dotc/typer/Typer.scala | 2 +- .../test/dotc/pos-test-pickling.blacklist | 3 + tests/pos/hylolib/AnyCollection.scala | 16 ++--- tests/pos/hylolib/AnyValueTests.scala | 15 +++++ tests/pos/hylolib/CollectionTests.scala | 67 +++++++++++++++++++ tests/pos/hylolib/Hasher.scala | 1 + tests/pos/hylolib/HyArray.scala | 1 - tests/pos/hylolib/HyArrayTests.scala | 17 +++++ tests/pos/hylolib/IntegersTests.scala | 14 ++++ tests/pos/hylolib/Test.scala | 16 +++++ tests/pos/i13580.scala | 13 ++++ tests/pos/parsercombinators-new-syntax.scala | 45 +++++++++++++ 12 files changed, 198 insertions(+), 12 deletions(-) create mode 100644 tests/pos/hylolib/AnyValueTests.scala create mode 100644 tests/pos/hylolib/CollectionTests.scala create mode 100644 tests/pos/hylolib/HyArrayTests.scala create mode 100644 tests/pos/hylolib/IntegersTests.scala create mode 100644 tests/pos/hylolib/Test.scala create mode 100644 tests/pos/i13580.scala create mode 100644 tests/pos/parsercombinators-new-syntax.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index ffc5ed1fb0ba..dcf9be3e4dc8 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2225,7 +2225,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } def typedRefinedTypeTree(tree: untpd.RefinedTypeTree)(using Context): TypTree = { - val tpt1 = if (tree.tpt.isEmpty) TypeTree(defn.ObjectType) else typedAheadType(tree.tpt) + val tpt1 = if tree.tpt == EmptyTree then TypeTree(defn.ObjectType) else typedAheadType(tree.tpt) val refineClsDef = desugar.refinedTypeToClass(tpt1, tree.refinements).withSpan(tree.span) val refineCls = createSymbol(refineClsDef).asClass val TypeDef(_, impl: Template) = typed(refineClsDef): @unchecked diff --git a/compiler/test/dotc/pos-test-pickling.blacklist b/compiler/test/dotc/pos-test-pickling.blacklist index 0297d2651e4b..f6f8a2514e5e 100644 --- a/compiler/test/dotc/pos-test-pickling.blacklist +++ b/compiler/test/dotc/pos-test-pickling.blacklist @@ -123,3 +123,6 @@ parsercombinators-givens.scala parsercombinators-ctx-bounds.scala parsercombinators-this.scala parsercombinators-arrow.scala +parsercombinators-new-syntax.scala +hylolib + diff --git a/tests/pos/hylolib/AnyCollection.scala b/tests/pos/hylolib/AnyCollection.scala index cec8b95ee077..6c2b835852e6 100644 --- a/tests/pos/hylolib/AnyCollection.scala +++ b/tests/pos/hylolib/AnyCollection.scala @@ -1,3 +1,4 @@ +//> using options -language:experimental.modularity -source future package hylo /** A type-erased collection. @@ -14,12 +15,7 @@ final class AnyCollection[Element] private ( object AnyCollection { /** Creates an instance forwarding its operations to `base`. */ - def apply[Base: Collection as b](base: Base): AnyCollection[b.Element] = - // NOTE: This evidence is redefined so the compiler won't report ambiguity between `intIsValue` - // and `anyValueIsValue` when the method is called on a collection of `Int`s. None of these - // choices is even correct! Note also that the ambiguity is suppressed if the constructor of - // `AnyValue` is declared with a context bound rather than an implicit parameter. - given b.Position is Value = b.Position + def apply[Base: Collection](base: Base): AnyCollection[Base.Element] = def start(): AnyValue = AnyValue(base.startPosition) @@ -28,12 +24,12 @@ object AnyCollection { AnyValue(base.endPosition) def after(p: AnyValue): AnyValue = - AnyValue(base.positionAfter(p.unsafelyUnwrappedAs[b.Position])) + AnyValue(base.positionAfter(p.unsafelyUnwrappedAs[Base.Position])) - def at(p: AnyValue): b.Element = - base.at(p.unsafelyUnwrappedAs[b.Position]) + def at(p: AnyValue): Base.Element = + base.at(p.unsafelyUnwrappedAs[Base.Position]) - new AnyCollection[b.Element]( + new AnyCollection[Base.Element]( _start = start, _end = end, _after = after, diff --git a/tests/pos/hylolib/AnyValueTests.scala b/tests/pos/hylolib/AnyValueTests.scala new file mode 100644 index 000000000000..96d3563f4f53 --- /dev/null +++ b/tests/pos/hylolib/AnyValueTests.scala @@ -0,0 +1,15 @@ +//> using options -language:experimental.modularity -source future +import hylo.* +import hylo.given + +class AnyValueTests extends munit.FunSuite: + + test("eq"): + val a = AnyValue(1) + assert(a `eq` a) + assert(!(a `neq` a)) + + val b = AnyValue(2) + assert(!(a `eq` b)) + assert(a `neq` b) + diff --git a/tests/pos/hylolib/CollectionTests.scala b/tests/pos/hylolib/CollectionTests.scala new file mode 100644 index 000000000000..d884790f64d7 --- /dev/null +++ b/tests/pos/hylolib/CollectionTests.scala @@ -0,0 +1,67 @@ +//> using options -language:experimental.modularity -source future +import hylo.* +import hylo.given + +class CollectionTests extends munit.FunSuite: + + test("isEmpty"): + val empty = AnyCollection(HyArray[Int]()) + assert(empty.isEmpty) + + val nonEmpty = AnyCollection(HyArray[Int](1, 2)) + assert(!nonEmpty.isEmpty) + + test("count"): + val a = AnyCollection(HyArray[Int](1, 2)) + assertEquals(a.count, 2) + + test("isBefore"): + val empty = AnyCollection(HyArray[Int]()) + assert(!empty.isBefore(empty.startPosition, empty.endPosition)) + + val nonEmpty = AnyCollection(HyArray[Int](1, 2)) + val p0 = nonEmpty.startPosition + val p1 = nonEmpty.positionAfter(p0) + val p2 = nonEmpty.positionAfter(p1) + assert(nonEmpty.isBefore(p0, nonEmpty.endPosition)) + assert(nonEmpty.isBefore(p1, nonEmpty.endPosition)) + assert(!nonEmpty.isBefore(p2, nonEmpty.endPosition)) + + test("headAndTail"): + val empty = AnyCollection(HyArray[Int]()) + assertEquals(empty.headAndTail, None) + + val one = AnyCollection(HyArray[Int](1)) + val Some((h0, t0)) = one.headAndTail: @unchecked + assert(h0 eq 1) + assert(t0.isEmpty) + + val two = AnyCollection(HyArray[Int](1, 2)) + val Some((h1, t1)) = two.headAndTail: @unchecked + assertEquals(h1, 1) + assertEquals(t1.count, 1) + + test("reduce"): + val empty = AnyCollection(HyArray[Int]()) + assertEquals(empty.reduce(0)((s, x) => s + x), 0) + + val nonEmpty = AnyCollection(HyArray[Int](1, 2, 3)) + assertEquals(nonEmpty.reduce(0)((s, x) => s + x), 6) + + test("forEach"): + val empty = AnyCollection(HyArray[Int]()) + assert(empty.forEach((e) => false)) + + val nonEmpty = AnyCollection(HyArray[Int](1, 2, 3)) + var s = 0 + assert(nonEmpty.forEach((e) => { s += e; true })) + assertEquals(s, 6) + + s = 0 + assert(!nonEmpty.forEach((e) => { s += e; false })) + assertEquals(s, 1) + + test("elementsEqual"): + val a = HyArray(1, 2) + assert(a.elementsEqual(a)) +end CollectionTests diff --git a/tests/pos/hylolib/Hasher.scala b/tests/pos/hylolib/Hasher.scala index ef6813df6b60..ca45550ed002 100644 --- a/tests/pos/hylolib/Hasher.scala +++ b/tests/pos/hylolib/Hasher.scala @@ -1,3 +1,4 @@ +//> using options -language:experimental.modularity -source future package hylo import scala.util.Random diff --git a/tests/pos/hylolib/HyArray.scala b/tests/pos/hylolib/HyArray.scala index ee2077089b2e..de5e83d3b1a3 100644 --- a/tests/pos/hylolib/HyArray.scala +++ b/tests/pos/hylolib/HyArray.scala @@ -56,7 +56,6 @@ final class HyArray[Element: Value as elementIsCValue]( result._count += 1 result - // NOTE: Can't refine `C.Element` without renaming the generic parameter of `HyArray`. /** Adds the contents of `source` at the end of the array. */ def appendContents[C: Collection { type Element = HyArray.this.Element }]( source: C, assumeUniqueness: Boolean = false diff --git a/tests/pos/hylolib/HyArrayTests.scala b/tests/pos/hylolib/HyArrayTests.scala new file mode 100644 index 000000000000..0de65603d0c7 --- /dev/null +++ b/tests/pos/hylolib/HyArrayTests.scala @@ -0,0 +1,17 @@ +import hylo.* +import hylo.given + +class HyArrayTests extends munit.FunSuite: + + test("reserveCapacity"): + var a = HyArray[Int]() + a = a.append(1) + a = a.append(2) + + a = a.reserveCapacity(10) + assert(a.capacity >= 10) + assertEquals(a.count, 2) + assertEquals(a.at(0), 1) + assertEquals(a.at(1), 2) + +end HyArrayTests diff --git a/tests/pos/hylolib/IntegersTests.scala b/tests/pos/hylolib/IntegersTests.scala new file mode 100644 index 000000000000..74dedf30d83e --- /dev/null +++ b/tests/pos/hylolib/IntegersTests.scala @@ -0,0 +1,14 @@ +//> using options -language:experimental.modularity -source future +import hylo.* +import hylo.given + +class IntegersTests extends munit.FunSuite: + + test("Int.hashInto"): + val x = Hasher.hash(42) + val y = Hasher.hash(42) + assertEquals(x, y) + + val z = Hasher.hash(1337) + assertNotEquals(x, z) + diff --git a/tests/pos/hylolib/Test.scala b/tests/pos/hylolib/Test.scala new file mode 100644 index 000000000000..9e8d6181affd --- /dev/null +++ b/tests/pos/hylolib/Test.scala @@ -0,0 +1,16 @@ +//> using options -language:experimental.modularity -source future +import hylo.* +import hylo.given + +object munit: + open class FunSuite: + def test(name: String)(op: => Unit): Unit = op + def assertEquals[T](x: T, y: T) = assert(x == y) + def assertNotEquals[T](x: T, y: T) = assert(x != y) + +@main def Test = + CollectionTests() + AnyValueTests() + HyArrayTests() + IntegersTests() + println("done") diff --git a/tests/pos/i13580.scala b/tests/pos/i13580.scala new file mode 100644 index 000000000000..c3c491a19dbe --- /dev/null +++ b/tests/pos/i13580.scala @@ -0,0 +1,13 @@ +//> using options -language:experimental.modularity -source future +trait IntWidth: + type Out +given IntWidth: + type Out = 155 + +trait IntCandidate: + type Out +given (using tracked val w: IntWidth) => IntCandidate: + type Out = w.Out + +val x = summon[IntCandidate] +val xx = summon[x.Out =:= 155] diff --git a/tests/pos/parsercombinators-new-syntax.scala b/tests/pos/parsercombinators-new-syntax.scala new file mode 100644 index 000000000000..93e8aac3bb7d --- /dev/null +++ b/tests/pos/parsercombinators-new-syntax.scala @@ -0,0 +1,45 @@ +//> using options -language:experimental.modularity -source future +import collection.mutable + +/// A parser combinator. +trait Combinator: + + type Self + type Input + type Result + + extension (self: Self) + /// Parses and returns an element from input `in`. + def parse(in: Input): Option[Result] +end Combinator + +case class Apply[C, E](action: C => Option[E]) +case class Combine[A, B](first: A, second: B) + +given [I, R] => Apply[I, R] is Combinator: + type Input = I + type Result = R + extension (self: Self) + def parse(in: I): Option[R] = self.action(in) + +given [A: Combinator, B: Combinator { type Input = A.Input }] + => Combine[A, B] is Combinator: + type Input = A.Input + type Result = (A.Result, B.Result) + extension (self: Self) + def parse(in: Input): Option[Result] = ??? + +extension [A] (buf: mutable.ListBuffer[A]) def popFirst() = + if buf.isEmpty then None + else try Some(buf.head) finally buf.remove(0) + +@main def hello: Unit = + val source = (0 to 10).toList + val stream = source.to(mutable.ListBuffer) + + val n = Apply[mutable.ListBuffer[Int], Int](s => s.popFirst()) + val m = Combine(n, n) + + val r = m.parse(stream) // was error: type mismatch, found `mutable.ListBuffer[Int]`, required `?1.Input` + val rc: Option[(Int, Int)] = r + From 5be4be46071c4254075b7b6d8f9818aebec4f5df Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 8 Jan 2024 00:00:10 +0100 Subject: [PATCH 31/63] Show that this solves #10929 --- tests/pos/i10929-new-syntax.scala | 22 ++++++++++++++++++++++ tests/pos/i10929.scala | 21 +++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 tests/pos/i10929-new-syntax.scala create mode 100644 tests/pos/i10929.scala diff --git a/tests/pos/i10929-new-syntax.scala b/tests/pos/i10929-new-syntax.scala new file mode 100644 index 000000000000..11c5e9313d4c --- /dev/null +++ b/tests/pos/i10929-new-syntax.scala @@ -0,0 +1,22 @@ +//> using options -language:experimental.modularity -source future +trait TupleOf[+A]: + type Self + type Mapped[+A] <: Tuple + def map[B](x: Self)(f: A => B): Mapped[B] + +object TupleOf: + + given EmptyTuple is TupleOf[Nothing]: + type Mapped[+A] = EmptyTuple + def map[B](x: EmptyTuple)(f: Nothing => B): Mapped[B] = x + + given [A, Rest <: Tuple : TupleOf[A]] => A *: Rest is TupleOf[A]: + type Mapped[+A] = A *: Rest.Mapped[A] + def map[B](x: A *: Rest)(f: A => B): Mapped[B] = + (f(x.head) *: Rest.map(x.tail)(f)) + +def foo[T: TupleOf[Int]](xs: T): T.Mapped[Int] = T.map(xs)(_ + 1) + +@main def test = + foo(EmptyTuple): EmptyTuple // ok + foo(1 *: EmptyTuple): Int *: EmptyTuple // now also ok diff --git a/tests/pos/i10929.scala b/tests/pos/i10929.scala new file mode 100644 index 000000000000..e916e4547e59 --- /dev/null +++ b/tests/pos/i10929.scala @@ -0,0 +1,21 @@ +//> using options -language:experimental.modularity -source future +infix abstract class TupleOf[T, +A]: + type Mapped[+A] <: Tuple + def map[B](x: T)(f: A => B): Mapped[B] + +object TupleOf: + + given TupleOf[EmptyTuple, Nothing] with + type Mapped[+A] = EmptyTuple + def map[B](x: EmptyTuple)(f: Nothing => B): Mapped[B] = x + + given [A, Rest <: Tuple](using tracked val tup: Rest TupleOf A): TupleOf[A *: Rest, A] with + type Mapped[+A] = A *: tup.Mapped[A] + def map[B](x: A *: Rest)(f: A => B): Mapped[B] = + (f(x.head) *: tup.map(x.tail)(f)) + +def foo[T](xs: T)(using tup: T TupleOf Int): tup.Mapped[Int] = tup.map(xs)(_ + 1) + +@main def test = + foo(EmptyTuple): EmptyTuple // ok + foo(1 *: EmptyTuple): Int *: EmptyTuple // now also ok \ No newline at end of file From 9fc21f9716c795e5bc051a57b37cfe090ebfa067 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 8 Jan 2024 00:02:49 +0100 Subject: [PATCH 32/63] Disambiguate more towards new given syntax, show solution for #15840 Faced with given C[T]: ... (with a new line after `:`) we now classify this as new given syntax, and assume ... is a template body. If one wants to use old syntax, one can still write given C[T] : ImplementedType ... # Conflicts: # compiler/src/dotty/tools/dotc/parsing/Parsers.scala --- .../dotty/tools/dotc/parsing/Parsers.scala | 1 - tests/run/i15840.scala | 27 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 tests/run/i15840.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index d228dc456af2..63c0e6facc23 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -965,7 +965,6 @@ object Parsers { lookahead.isColon && { !in.featureEnabled(Feature.modularity) - || paramsSeen || { // with modularity language import, a `:` at EOL after an identifier represents a single identifier given // Example: // given C: diff --git a/tests/run/i15840.scala b/tests/run/i15840.scala new file mode 100644 index 000000000000..0f238e2e7148 --- /dev/null +++ b/tests/run/i15840.scala @@ -0,0 +1,27 @@ +//> using options -language:experimental.modularity -source future + +trait Nat: + type N <: Nat + +class _0 extends Nat: + type N = _0 + +class NatOps[N <: Nat](tracked val n: N): + def toInt(using toIntN: ToInt[n.N]): Int = toIntN() + +// works +def toInt[N <: Nat](n: N)(using toIntN: ToInt[n.N]) = toIntN() + +sealed abstract class ToInt[N <: Nat]: + def apply(): Int + +object ToInt: + given ToInt[_0] { + def apply() = 0 + } + +@main def Test() = + assert(toInt(new _0) == 0) + assert(NatOps[_0](new _0).toInt == 0) + assert: + NatOps(new _0).toInt == 0 // did not work From d5f089d61df0d92aa64c335939d2194fcffab1c7 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 6 Jan 2024 13:53:17 +0100 Subject: [PATCH 33/63] Also reduce term projections We already reduce `R { type A = T } # A` to `T` in most situations when we create types. We now also reduce `R { val x: S } # x` to `S` if `S` is a singleton type. This will simplify types as we go to more term-dependent typing. As a concrete benefit, it will avoid several test-pickling failures due to pickling differences when using dependent types. --- .../src/dotty/tools/dotc/core/Types.scala | 66 +++++++++---------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 62844a54bf48..eed9af859296 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1631,17 +1631,19 @@ object Types extends TypeUtils { * * P { ... type T = / += / -= U ... } # T * - * to just U. Does not perform the reduction if the resulting type would contain - * a reference to the "this" of the current refined type, except in the following situation + * to just U. Analogously, `P { val x: S} # x` is reduced tp `S` is `S` + * is a singleton type. * - * (1) The "this" reference can be avoided by following an alias. Example: + * Does not perform the reduction if the resulting type would contain + * a reference to the "this" of the current refined type, except if the "this" + * reference can be avoided by following an alias. Example: * * P { type T = String, type R = P{...}.T } # R --> String * * (*) normalizes means: follow instantiated typevars and aliases. */ - def lookupRefined(name: Name)(using Context): Type = { - @tailrec def loop(pre: Type): Type = pre.stripTypeVar match { + def lookupRefined(name: Name)(using Context): Type = + @tailrec def loop(pre: Type): Type = pre match case pre: RefinedType => pre.refinedInfo match { case tp: AliasingBounds => @@ -1664,12 +1666,13 @@ object Types extends TypeUtils { case TypeAlias(alias) => loop(alias) case _ => NoType } + case pre: (TypeVar | AnnotatedType) => + loop(pre.underlying) case _ => NoType - } loop(this) - } + end lookupRefined /** The type , reduced if possible */ def select(name: Name)(using Context): Type = @@ -2809,35 +2812,30 @@ object Types extends TypeUtils { def derivedSelect(prefix: Type)(using Context): Type = if prefix eq this.prefix then this else if prefix.isExactlyNothing then prefix - else { - val res = - if (isType && currentValidSymbol.isAllOf(ClassTypeParam)) argForParam(prefix) + else + val reduced = + if isType && currentValidSymbol.isAllOf(ClassTypeParam) then argForParam(prefix) else prefix.lookupRefined(name) - if (res.exists) return res - if (isType) { - if (Config.splitProjections) - prefix match { - case prefix: AndType => - def isMissing(tp: Type) = tp match { - case tp: TypeRef => !tp.info.exists - case _ => false - } - val derived1 = derivedSelect(prefix.tp1) - val derived2 = derivedSelect(prefix.tp2) - return ( - if (isMissing(derived1)) derived2 - else if (isMissing(derived2)) derived1 - else prefix.derivedAndType(derived1, derived2)) - case prefix: OrType => - val derived1 = derivedSelect(prefix.tp1) - val derived2 = derivedSelect(prefix.tp2) - return prefix.derivedOrType(derived1, derived2) - case _ => - } - } - if (prefix.isInstanceOf[WildcardType]) WildcardType.sameKindAs(this) + if reduced.exists then return reduced + if Config.splitProjections && isType then + prefix match + case prefix: AndType => + def isMissing(tp: Type) = tp match + case tp: TypeRef => !tp.info.exists + case _ => false + val derived1 = derivedSelect(prefix.tp1) + val derived2 = derivedSelect(prefix.tp2) + return + if isMissing(derived1) then derived2 + else if isMissing(derived2) then derived1 + else prefix.derivedAndType(derived1, derived2) + case prefix: OrType => + val derived1 = derivedSelect(prefix.tp1) + val derived2 = derivedSelect(prefix.tp2) + return prefix.derivedOrType(derived1, derived2) + case _ => + if prefix.isInstanceOf[WildcardType] then WildcardType.sameKindAs(this) else withPrefix(prefix) - } /** A reference like this one, but with the given symbol, if it exists */ private def withSym(sym: Symbol)(using Context): ThisType = From 399ba70ccd30c855a3d4ea251b334be9819613f6 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 6 Jan 2024 17:13:02 +0100 Subject: [PATCH 34/63] Refine rules when context bound evidence is tracked Exclude synthetic, unnamed context bound evidence parameters. --- compiler/src/dotty/tools/dotc/typer/Namer.scala | 12 +++++++----- tests/pos/parsercombinators-new-syntax.scala | 10 +++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 09324e9d42b6..3cc8341f3d5b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -1862,11 +1862,12 @@ class Namer { typer: Typer => sym.setParamss(paramSymss) /** Set every context bound evidence parameter of a class to be tracked, - * provided it has a type that has an abstract type member. Reset private - * and local flags so that the parameter becomes a `val`. Do the same for all - * context bound evidence parameters of a `given` class. This is because - * in Desugar.addParamRefinements we create refinements for these parameters - * in the result type of the implicit access method. + * provided it has a type that has an abstract type member and the parameter's + * name is not a synthetic, unaddressable name. Reset private and local flags + * so that the parameter becomes a `val`. Do the same for all context bound + * evidence parameters of a `given` class. This is because in Desugar.addParamRefinements + * we create refinements for these parameters in the result type of the implicit + * access method. */ def setTracked(param: ValDef): Unit = val sym = symbolOfTree(param) @@ -1874,6 +1875,7 @@ class Namer { typer: Typer => case info: TempClassInfo => if !sym.is(Tracked) && param.hasAttachment(ContextBoundParam) + && !param.name.is(ContextBoundParamName) && (sym.info.memberNames(abstractTypeNameFilter).nonEmpty || sym.maybeOwner.maybeOwner.is(Given)) then diff --git a/tests/pos/parsercombinators-new-syntax.scala b/tests/pos/parsercombinators-new-syntax.scala index 93e8aac3bb7d..f984972b915d 100644 --- a/tests/pos/parsercombinators-new-syntax.scala +++ b/tests/pos/parsercombinators-new-syntax.scala @@ -3,7 +3,6 @@ import collection.mutable /// A parser combinator. trait Combinator: - type Self type Input type Result @@ -13,21 +12,22 @@ trait Combinator: def parse(in: Input): Option[Result] end Combinator -case class Apply[C, E](action: C => Option[E]) +case class Apply[I, R](action: I => Option[R]) case class Combine[A, B](first: A, second: B) given [I, R] => Apply[I, R] is Combinator: type Input = I type Result = R - extension (self: Self) + extension (self: Apply[I, R]) def parse(in: I): Option[R] = self.action(in) given [A: Combinator, B: Combinator { type Input = A.Input }] => Combine[A, B] is Combinator: type Input = A.Input type Result = (A.Result, B.Result) - extension (self: Self) - def parse(in: Input): Option[Result] = ??? + extension (self: Combine[A, B]) + def parse(in: Input): Option[Result] = + for x <- self.first.parse(in); y <- self.second.parse(in) yield (x, y) extension [A] (buf: mutable.ListBuffer[A]) def popFirst() = if buf.isEmpty then None From 7cfc15076b16e965ef00f4e3eeaa75c3f43888cb Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 8 Jan 2024 14:00:39 +0100 Subject: [PATCH 35/63] Add a doc page --- .../reference/experimental/typeclasses.md | 672 ++++++++++++++++++ docs/sidebar.yml | 1 + 2 files changed, 673 insertions(+) create mode 100644 docs/_docs/reference/experimental/typeclasses.md diff --git a/docs/_docs/reference/experimental/typeclasses.md b/docs/_docs/reference/experimental/typeclasses.md new file mode 100644 index 000000000000..d41df048f267 --- /dev/null +++ b/docs/_docs/reference/experimental/typeclasses.md @@ -0,0 +1,672 @@ + +--- +layout: doc-page +title: "Type Classes" +nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/typeclasses.html +--- + +# Some Proposed Changes for Better Support of Type Classes + +Martin Odersky, 8.1.2024 + +A type class in Scala is a pattern where we define + + - a trait with one type parameter (the _type class_) + - given instances at specific instantiations of that trait, + - using clauses or context bounds abstracting over that trait. + +Type classes as a pattern work overall OK, but if we compare them to native implementations in Haskell, or protocols in Swift, or traits in Rust, then there are some idiosyncracies and rough corners which in the end make them +a bit cumbersome and limiting for standard generic programming patterns. Much has improved since Scala 2's implicits, but there is still some gap to bridge to get to parity with these languages. + +This note shows that with some fairly small and reasonable tweaks to Scala's syntax and typing rules we can obtain a much better scheme for working with type classes, or do generic programming in general. + +The bulk of the suggested improvements has been implemented and is available +under source version `future` if the additional experimental language import `modularity` is present. For instance, using the following command: + +``` + scala compile -source:future -language:experimental.modularity +``` + +## Generalizing Context Bounds + + The only place in Scala's syntax where the type class pattern is relevant is + in context bounds. A context bound such as + +```scala + def min[A: Ordering](x: List[A]): A +``` +requires that `Ordering` is a trait or class with a single type parameter (which makes it a type class) and expands to a `using` clause that instantiates that parameter. Here is the expansion of `min`: +```scala + def min[A](x: List[A])(using Ordering[A]): A +``` + +**Proposal** Allow type classes to define an abstract type member named `Self` instead of a type parameter. + +**Example** + +```scala + trait Ord: + type Self + + trait SemiGroup: + type Self + extension (x: Self) def combine(y: Self): Self + + trait Monoid extends SemiGroup: + def unit: Self + object Monoid + def unit(using m: Monoid): m.Self = m.unit + + trait Functor: + type Self[A] + extension [A](x: Self[A]) def map[B](f: A => B): Self[B] + + trait Monad extends Functor: + def pure[A](x: A): Self[A] + extension [A](x: Self[A]) + def flatMap[B](f: A => Self[B]): Self[B] + def map[B](f: A => B) = x.flatMap(f `andThen` pure) + + def reduce[A: Monoid](xs: List[A] = + xs.foldLeft(Monoid.unit)(_ `combine` _) + + trait ParserCombinator: + type Self + type Input + type Result + extension (self: Self) + def parse(input: Input): Option[Result] = ... + + def combine[A: ParserCombinator, B: ParserCombinator { type Input = A.Input }] = ... +``` + +**Advantages** + + - Avoids repetitive type parameters, concentrates on what's essential, namely the type class hierarchy. + - Gives a clear indication of traits intended as type classes. A trait is a type class + if it has type `Self` as a member + - Allows to create aggregate type classes that combine givens via intersection types. + - Allows to use refinements in context bounds (the `combine` example above would be very awkward to express using the old way of context bounds expanding to type copnstructors). + +`Self`-based context bounds are a better fit for a dependently typed language like Scala than parameter-based ones. The main reason is that we are dealing with proper types, not type constructors. Proper types can be parameterized, intersected, or refined. This makes `Self`-based designs inherently more compositional than parameterized ones. + + + +**Details** + +When a trait has both a type parameter and an abstract `Self` type, we + resolve a context bound to the `Self` type. This allows type classes + that carry type parameters, as in + +```scala +trait Sequential[E]: + type Self +``` + +Here, +```scala +[S: Sequential[Int]] +``` +should resolve to: +```scala +[S](using Sequential[Int] { type Self = S }) +``` +and not to: +```scala +[S](using Sequential[S] +``` + +**Discussion** + + Why not use `This` for the self type? The name `This` suggests that it is the type of `this`. But this is not true for type class traits. `Self` is the name of the type implementing a distinguished _member type_ of the trait in a `given` definition. `Self` is an established term in both Rust and Swift with the meaning used here. + + One possible objection to the `Self` based design is that it does not cover "multi-parameter" type classes. But neither do context bounds! "Multi-parameter" type classes in Scala are simply givens that can be synthesized with the standard mechanisms. Type classes in the strict sense abstract only over a single type, namely the implementatation type of a trait. + + +## Auxiliary Type Alias `is` + +We introduce a standard type alias `is` in the Scala package or in `Predef`, defined like this: + +```scala + infix type is[A <: AnyKind, B <: {type Self <: AnyKind}] = B { type Self = A } +``` + +This makes writing instance definitions quite pleasant. Examples: + +```scala + given Int is Ord ... + given Int is Monoid ... + + type Reader = [X] =>> Env => X + given Reader is Monad ... +``` + +(more examples will follow below) + + + +## Naming Context Bounds + +Context bounds are a convenient and legible abbreviation. A problem so far is that they are always anonymous, one cannot name the using parameter to which a context bound expands. For instance, without the trick of defining a universal "trampoline" `unit` in the `Monoid` companion object, we would have to write `reduce` like this: +```scala + def reduce[A](xs: List[A])(using m: A is Monoid) = + xs.foldLeft(m.unit)(_ `combine` _) +``` + +**Proposal:** Allow to name a context bound, like this: +```scala + def reduce[M: Monoid as m](xs: List[M] = + xs.foldLeft(m.unit)(_ `combine` _) +``` + +We use `as x` after the type to bind the instance to `x`. This is analogous to import renaming, which also introduces a new name for something that comes before. + +## New Syntax for Aggregate Context Bounds + +Aggregate context bounds like `A: X: Y` are not obvious to read, and it becomes worse when we add names, e.g. `A : X as x : Y as y`. + +**Proposal:** Allow to combine several context bounds inside `{...}`, analogous +to import clauses. + +```scala + def showMax[X : {Ordered as ordering, Show as show}](x: X, y: X): String = + show.asString(ordering.max(x, y)) +``` + +The old syntax with multiple `:` should be phased out over time. + +## Better Default Names for Context Bounds + +So far, a context bound for a type `M` gets a synthesized fresh name. It would be much more useful if it got the name of the constrained type instead, translated to be a term name. This means our `reduce` method over monoids would not even need an `as` binding. We could simply formulate it as follows: +``` + def reduce[M: Monoid](xs: List[M] = + xs.foldLeft(M.unit)(_ `combine` _) +``` + +**Proposed Rules** + + 1. The generated evidence parameter for a context bound `M: C as m` has name `m` + 2. The generated evidence for a context bound `M: C` without an `as` binding has name `M` (seen as a term name). So, `M: C` is equivalent to `M: C as M`. + 3. If there are more than one context bounds for a type parameter, the generated evidence parameter for every context bound except the first one has a fresh synthesized name, unless the context bound carries an `as` clause, in which case rule (1) applies. + +## Expansion of Context Bounds + +Context bounds are currently translated to implicit parameters in the last parameter list of a method or class. This is a problem if a context bound is mentioned in one of the preceding parameter types. Example: +```scala + def f[C: ParserCombinator](x: C.Input) = ... +``` +With the current translation, this would give +```scala + def f[C](x: C.input)(using C: C is ParserCombinator) +``` +But this is ill-typed, since the `C` in `C.input` refers to the `C` introduced in the using clause, which comes later. + +This problem would be fixed by changing the translation of context bounds so that they expand to using clauses immediately after the type parameter. But such a change is +infeasible, for two reasons: + + 1. It would be a binary-incompatible change. + 2. Putting using clauses earlier can impair type inference. A type in + a using clause can be constrained by term arguments coming before that + clause. Moving the using clause first would miss those constraints, which + could cause ambiguities in implicit search. + +But there is an alternative which is feasible: + +**Proposal:** Map the context bounds of a method or class as follows: + + 1. If one of the bounds is referred to by its term name in a subsequent parameter clause, the context bounds are mapped to a using clause immediately preceding the first such parameter clause. + 2. Otherwise, if the last parameter clause is a using (or implicit) clause, merge all parameters arising from context bounds in front of that clause, creating a single using clause. + 3. Otherwise, let the parameters arising from context bounds form a new using clause at the end. + +Rules (2) and (3) are the status quo, and match Scala 2's rules. Rule (1) is new but since context bounds so far could not be referred to, it does not apply to legacy code. Therefore, binary compatibility is maintained. + +**Discussion** More refined rules could be envisaged where context bounds are spread over different using clauses so that each comes as late as possible. But it would make matters more complicated and the gain in expressiveness is not clear to me. + +Named (either explicitly, or by default) context bounds in givens that produce classes are mapped to tracked val's of these classes (see #18958). This allows +references to these parameters to be precise, so that information about dependent type members is preserved. + + +## Context Bounds for Type Members + +It's not very orthogonal to allow subtype bounds for both type parameters and abstract type members, but context bounds only for type parameters. By moving more of the type class logic to type members this lack becomes more of a problem. + +**Proposal**: Allow context bounds for type members. Example: + +```scala + class C: + type Element: Ordered +``` + +The question is how these bounds are expanded. Context bounds on type parameters +are expanded into using clauses. But for type members this does not work, since we cannot refer to a member type of a class in a parameter type of that class. What we are after is an equivalent of using parameter clauses but represented as class members. This is basically a list of deferred givens which can be filled automatically on object creation. + +**Proposal:** Introduce a new kind of given definition of the form: +```scala +given T = deferred +``` +`deferred` is a soft keyword which has special meaning only in this context. +A given with `deferred` right hand side can appear only as a member definition of some trait. Any class implementing that trait will provide an implementation of this given. If a definition is not provided explicitly, it will be synthesized by searching for a given of type `T` in the scope of the inheriting class `C`. Specifically, the scope in which this given will be searched is the environment of `C` augmented by the parameters of `C` but not containing the members of `C` (since that would lead to recursive resolutions). + +A context bound +```scala +type T: C +``` +in a trait will then expand to +```scala +type T +given T is C = deferred +``` + + +## New Given Syntax + +Our goal is to revise the `given` syntax so that conditional givens don't have to be written anymore like this: + +```scala +given [A](using Ord[A]): Ord[List[A]] with + def compare(x: List[A], y: List[A]) = ... +``` +or, using an explicit name, like this: +```scala +given listOrd[A](using Ord[A]): Ord[List[A]] with + def compare(x: List[A], y: List[A]) = ... +``` +The named version below makes sort of sense, but the unnamed version above is annoyingly irregular compared to other Scala language elements. In particular: + + - There is the `:` that separates parameter clauses and the implemented type class. + This feels out of place, in particular in the unnamed version. + - There is the `with` at the end, which is the only place in Scala where `with` starts a block with definitions. Everywhere else we use `:` or `=`. + +The awkwardness of the given syntax was forced upon us since we insisted that givens could be named or anonymous, that we would not use underscore for an anonymous given, and that the name, if present, had to come first. Things become much simpler if we introduce the optional name instead with an `as name` clause at the end. We get uniformity with context bounds, +and we can use a more intuitive syntax for givens like this: + +```scala +given Int is Ord: + def compare(x: A, y: A) = ... + +given [A: Ord] => List[A] is Ord: + def compare(x: A, y: A) = + ... + +given Int is Monoid as intMonoid: + extension (x: Int) def combine(y: Int) = x + y + def unit = 0 +``` + +The underlying principles are: + + - A `given` clause consists of the following elements: + + - An optional _precondition_, which introduces type parameters and/or using clauses and which ends in `=>`, + - the implemented _type_, + - an optional name binding using `as`, + - an implementation which consists of either an `=` and an expression, + or a template body. + + - We get the pleasing ` is ` syntax simply by using the predefined infix type `is`. + - Since there is no more middle `:` separating name and parameters from the implemented type, we can use a `:` to start the class body without looking unnatural. That eliminates the special case where `with` was used before. + +This will be a fairly significant change to the given syntax. I believe there's still a possibility to do this, +if we are convinced that this is a clear improvement. Not so much code has migrated to new style givens yet, and code that was written can probably be changed fairly easily. But we the longer we wait, the harder it would get. + +Migration to the new syntax should be straightforward. For a transition period we can support both the old and the new syntax. And we can offer rewrite rules that turn old into new. + +## Abolish Abstract Givens + +So far we have special syntax for abstract givens: +```scala +given x: T +``` +This should be no longer necessary with the `deferred` scheme, so I propose to deprecate that syntax (over time). Abstract givens are a highly specialized mechanism with a (so far) non-obvious syntax. My estimate is that maybe a dozen people world-wide have used them in anger so far. + +**Proposal** In the future, let the `= deferred` mechanism be the only way to define an abstract given. + +This is less of a disruption than it might appear at first: + + - `given T` was illegal before since abstract givens could not be anonymous. + It now means a concrete given of class `T` with no member definitions. This + is the natural interpretation for simple tagging given clauses such as + `given String is Value`. + - `given x: T` is legacy syntax for a deferred given. + - `given T as x = deferred` is the analogous new syntax, which is more powerful since + it allows for automatic instantiation. + - `given T = deferred` is the anonymous version in the new syntax, which was not expressible before. + +## Syntax Changes + +The changed syntax is here. Overall the syntax for givens becomes a lot simpler than what it was before. + +``` +TmplDef ::= 'given' GivenDef +GivenDef ::= [GivenConditional '=>'] GivenSig +GivenConditional ::= [DefTypeParamClause | UsingParamClause] {UsingParamClause} +GivenSig ::= GivenType ['as' id] ([‘=’ Expr] | TemplateBody) + | ConstrApps ['as' id] TemplateBody +GivenType ::= AnnotType {id [nl] AnnotType} + +TypeParamBounds ::= TypeBounds [‘:’ ContextBounds] +ContextBounds ::= ContextBound | '{' ContextBound {',' ContextBound} '}' +ContextBound ::= Type ['as' id] +``` + + + +## Examples + + +### Example 1 + +Here are some standard type classes, which were mostly already introduced at the start of this note, now with associated instance givens and some test code: + +```scala + // Type classes + + trait Ord: + type Self + extension (x: Self) + def compareTo(y: Self): Int + def < (y: Self): Boolean = compareTo(y) < 0 + def > (y: Self): Boolean = compareTo(y) > 0 + def <= (y: Self): Boolean = compareTo(y) <= 0 + def >= (y: Self): Boolean = compareTo(y) >= 0 + def max(y: Self): Self = if x < y then y else x + + trait Show: + type Self + extension (x: Self) def show: String + + trait SemiGroup: + type Self + extension (x: Self) def combine(y: Self): Self + + trait Monoid extends SemiGroup: + def unit: Self + + trait Functor: + type Self[A] + extension [A](x: Self[A]) def map[B](f: A => B): Self[B] + + trait Monad extends Functor: + def pure[A](x: A): Self[A] + extension [A](x: Self[A]) + def flatMap[B](f: A => Self[B]): Self[B] + def map[B](f: A => B) = x.flatMap(f `andThen` pure) + + // Instances + + given Int is Ord: + extension (x: Int) + def compareTo(y: Int) = + if x < y then -1 + else if x > y then +1 + else 0 + + given [T: Ord] => List[T] is Ord: + extension (xs: List[T]) def compareTo(ys: List[T]): Int = + (xs, ys) match + case (Nil, Nil) => 0 + case (Nil, _) => -1 + case (_, Nil) => +1 + case (x :: xs1, y :: ys1) => + val fst = x.compareTo(y) + if (fst != 0) fst else xs1.compareTo(ys1) + + given List is Monad: + extension [A](xs: List[A]) + def flatMap[B](f: A => List[B]): List[B] = + xs.flatMap(f) + def pure[A](x: A): List[A] = + List(x) + + type Reader[Ctx] = [X] =>> Ctx => X + + given [Ctx] => Reader[Ctx] is Monad: + extension [A](r: Ctx => A) + def flatMap[B](f: A => Ctx => B): Ctx => B = + ctx => f(r(ctx))(ctx) + def pure[A](x: A): Ctx => A = + ctx => x + + // Usages + + extension (xs: Seq[String]) + def longestStrings: Seq[String] = + val maxLength = xs.map(_.length).max + xs.filter(_.length == maxLength) + + extension [M[_]: Monad, A](xss: M[M[A]]) + def flatten: M[A] = + xss.flatMap(identity) + + def maximum[T: Ord](xs: List[T]): T = + xs.reduce(_ `max` _) + + given [T: Ord] => T is Ord as descending: + extension (x: T) def compareTo(y: T) = T.compareTo(y)(x) + + def minimum[T: Ord](xs: List[T]) = + maximum(xs)(using descending) +``` + + +### Example 2 + +The following contributed code by @LPTK (issue #10929) did _not_ work at first since +references were not tracked correctly. The version below adds explicit tracked parameters which makes the code compile. +```scala +infix abstract class TupleOf[T, +A]: + type Mapped[+A] <: Tuple + def map[B](x: T)(f: A => B): Mapped[B] + +object TupleOf: + + given TupleOf[EmptyTuple, Nothing] with + type Mapped[+A] = EmptyTuple + def map[B](x: EmptyTuple)(f: Nothing => B): Mapped[B] = x + + given [A, Rest <: Tuple](using tracked val tup: Rest TupleOf A): TupleOf[A *: Rest, A] with + type Mapped[+A] = A *: tup.Mapped[A] + def map[B](x: A *: Rest)(f: A => B): Mapped[B] = + f(x.head) *: tup.map(x.tail)(f) +``` + +Note the quite convoluted syntax, which makes the code hard to understand. Here is the same example in the new type class syntax, which also compiles correctly: +```scala +//> using options -language:experimental.modularity -source future + +trait TupleOf[+A]: + type Self + type Mapped[+A] <: Tuple + def map[B](x: Self)(f: A => B): Mapped[B] + +object TupleOf: + + given EmptyTuple is TupleOf[Nothing]: + type Mapped[+A] = EmptyTuple + def map[B](x: EmptyTuple)(f: Nothing => B): Mapped[B] = x + + given [A, Rest <: Tuple : TupleOf[A]] => A *: Rest is TupleOf[A]: + type Mapped[+A] = A *: Rest.Mapped[A] + def map[B](x: A *: Rest)(f: A => B): Mapped[B] = + f(x.head) *: Rest.map(x.tail)(f) +``` +Note in particular the following points: + + - In the original code, it was not clear that `TupleOf` is a type class, + since it contained two type parameters, one of which played the role + of the instance type `Self`. The new version is much clearer: `TupleOf` is + a type class over `Self` with one additional parameter, the common type of all tuple elements. + - The two given definitions are obfuscated in the old code. Their version + in the new code makes it clear what kind of instances they define: + + - `EmptyTuple` is a tuple of `Nothing`. + - if `Rest` is a tuple of `A`, then `A *: Rest` is also a tuple of `A`. + + - There's no need to introduce names for parameter instances in using clauses; the default naming scheme for context bound evidences works fine, and is more concise. + - There's no need to manually declare implicit parameters as `tracked`, + context bounds provide that automatically. + - Everything in the new code feels like idiomatic Scala 3, whereas the original code exhibits the awkward corner case that requires a `with` in + front of given definitions. + +### Example 3 + +Dimi Racordon tried to [define parser combinators](https://users.scala-lang.org/t/create-an-instance-of-a-type-class-with-methods-depending-on-type-members/9613) in Scala that use dependent type members for inputs and results. It was intended as a basic example of type class constraints, but it did not work in current Scala. + +Here is the problem solved with the new syntax. Note how much clearer that syntax is compared to Dimi's original version, which did not work out in the end. + +```scala +/** A parser combinator */ +trait Combinator: + type Self + + type Input + type Result + + extension (self: Self) + /** Parses and returns an element from input `in` */ + def parse(in: Input): Option[Result] +end Combinator + +case class Apply[I, R](action: I => Option[R]) +case class Combine[A, B](a: A, b: B) + +given [I, R] => Apply[I, R] is Combinator: + type Input = I + type Result = R + extension (self: Apply[I, R]) + def parse(in: I): Option[R] = self.action(in) + +given [A: Combinator, B: Combinator { type Input = A.Input }] + => Combine[A, B] is Combinator: + type Input = A.Input + type Result = (A.Result, B.Result) + extension (self: Combine[A, B]) + def parse(in: Input): Option[Result] = + for + x <- self.a.parse(in) + y <- self.b.parse(in) + yield (x, y) +``` +The example is now as expressed as straighforwardly as it should be: + + - `Combinator` is a type class with two associated types, `Input` and `Result`, and a `parse` method. + - `Apply` and `Combine` are two data constructors representing parser combinators. They are declared to be `Combinators` in the two subsequent `given` declarations. + - `Apply`'s parse method applies the `action` function to the input. + - `Combine[A, B]` is a parser combinator provided `A` and `B` are parser combinators + that process the same type of `Input`, which is also the input type of + `Combine[A, B]`. Its `Result` type is a pair of the `Result` types of `A` and `B`. + Results are produced by a simple for-expression. + +Compared to the original example, which required serious contortions, this is now all completely straightforward. + +_Note 1:_ One could also explore improvements, for instance making this purely functional. But that's not the point of the demonstration here, where I wanted +to take the original example and show how it can be made to work with the new constructs, and be expressed more clearly as well. + +_Note 2:_ One could improve the notation even further by adding equality constraints in the style of Swift, which in turn resemble the _sharing constraints_ of SML. A hypothetical syntax applied to the second given would be: +```scala +given [A: Combinator, B: Combinator with A.Input == B.Input] + => Combine[A, B] is Combinator: +``` +This variant is esthetically pleasing since it makes the equality constraint symmetric. The original version had to use an asymmetric refinement on the second type parameter bound instead. For now, such constraints are neither implemented nor proposed. This is left as a possibility for future work. Note also the analogy with +the work of @mbovel and @Sporarum on refinement types, where similar `with` clauses can appear for term parameters. If that work goes ahead, we could possibly revisit the issue of `with` clauses also for type parameters. + +### Example 4 + +Dimi Recordon tried to [port some core elenents](https://github.com/kyouko-taiga/scala-hylolib) of the type class based [Hylo standard library to Scala](https://github.com/hylo-lang/hylo/tree/main/StandardLibrary/Sources). It worked to some degree, but there were some things that could not be expressed, and more things that could be expressed only awkwardly. + +With the improvements proposed here, the library can now be expressed quite clearly and straightforwardly. See tests/pos/hylolib in this PR for details. + +## Suggested Improvements unrelated to Type Classes + +The following improvements elsewhere would make sense alongside the suggested changes to type classes. But they are currently not part of this proposal or implementation. + +### Fixing Singleton + +We know the current treatment of `Singleton` as a type bound is broken since +`x.type | y.type <: Singleton` holds by the subtyping rules for union types, even though `x.type | y.type` is clearly not a singleton. + +A better approach is to treat `Singleton` as a type class that is interpreted specially by the compiler. + +We can do this in a backwards-compatible way by defining `Singleton` like this: + +```scala +trait Singleton: + type Self +``` + +Then, instead of using an unsound upper bound we can use a context bound: + +```scala +def f[X: Singleton](x: X) = ... +``` + +The context bound would be treated specially by the compiler so that no using clause is generated at runtime. + +_Aside_: This can also lead to a solution how to express precise type variables. We can introduce another special type class `Precise` and use it like this: + +```scala +def f[X: Precise](x: X) = ... +``` +This would disable automatic widening of singleton types in inferred instances of type variable `X`. + +### Using `as` also in Patterns + +Since we have now more precedents of `as` as a postfix binder, I want to come back to the proposal to use it in patterns as well, in favor of `@`, which should be deprecated. + +Examples: + +```scala + xs match + case (Person(name, age) as p) :: rest => ... + + tp match + case Param(tl, _) :: _ as tparams => ... + + val x :: xs1 as xs = ys.checkedCast +``` + +These would replace the previous syntax using `@`: + +```scala + xs match + case p @ Person(name, age) :: rest => ... + + tp match + case tparams @ (Param(tl, _) :: _) => ... + + val xs @ (x :: xs1) = ys.checkedCast +``` +**Advantages:** No unpronouncible and non-standard symbol like `@`. More regularity. + +Generally, we want to use `as name` to attach a name for some entity that could also have been used stand-alone. + +**Proposed Syntax Change** + +``` +Pattern2 ::= InfixPattern ['as' id] +``` + +## Summary + +I have proposed some tweaks to Scala 3, which would greatly increase its usability for modular, type class based, generic programming. The proposed changes are: + + 1. Allow context bounds over classes that define a `Self` member type. + 1. Allow context bounds to be named with `as`. Use the bound parameter name as a default name for the generated context bound evidence. + 1. Add a new `{...}` syntax for multiple context bounds. + 1. Make context bounds also available for type members, which expand into a new form of deferred given. Phase out the previous abstract givens in favor of the new form. + 1. Add a predefined type alias `is`. + 1. Introduce a new cleaner syntax of given clauses. + +It's interesting that givens, which are a very general concept in Scala, were "almost there" when it comes to full support of concepts and generic programming. We only needed to add a few usability tweaks to context bounds, +alongside two syntactic changes that supersede the previous forms of `given .. with` clauses and abstract givens. Also interesting is that the superseded syntax constructs were the two areas where we collectively felt that the previous solutions were a bit awkward, but we could not think of better ones at the time. It's very nice that more satisfactory solutions are now emerging. + +## Conclusion + +Generic programming can be expressed in a number of languages. For instance, with +type classes in Haskell, or with traits in Rust, or with protocols in Swift, or with concepts in C++. Each of these is constructed from a fairly heavyweight set of new constructs, different from expressions and types. By contrast, equivalent solutions in Scala rely on regular types. Type classes are simply traits that define a `Self` type member. + +The proposed scheme has similar expressiveness to Protocols in Swift or Traits in Rust. Both of these were largely influenced by Jeremy Siek's PdD thesis "[A language for generic programming](https://scholarworks.iu.edu/dspace/handle/2022/7067)", which was first proposed as a way to implement concepts in C++. C++ did not follow Siek's approach, but Swift and Rust did. + +In Siek's thesis and in the formal treatments of Rust and Swift, + type class concepts are explained by mapping them to a lower level language of explicit dictionaries with representants for terms and types. Crucially, that lower level is not expressible without loss of granularity in the source language itself, since type representants are mapped to term dictionaries. By contrast, the current proposal expands type class concepts into other well-typed Scala constructs, which ultimately map into well-typed DOT programs. Type classes are simply a convenient notation for something that can already be expressed in Scala. In that sense, we stay true to the philosophy of a _scalable language_, where a small core can support a large range of advanced use cases. + diff --git a/docs/sidebar.yml b/docs/sidebar.yml index 7d2ff2532c04..6b8f8d561f82 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -154,6 +154,7 @@ subsection: - page: reference/experimental/purefuns.md - page: reference/experimental/tupled-function.md - page: reference/experimental/modularity.md + - page: reference/experimental/typeclasses.md - page: reference/syntax.md - title: Language Versions index: reference/language-versions/language-versions.md From b1a47aacc349c85999405ad9f4190c9987462ea9 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 8 Jan 2024 15:05:45 +0100 Subject: [PATCH 36/63] Do deferred given resolution only at typer phase # Conflicts: # compiler/src/dotty/tools/dotc/typer/Typer.scala --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index dcf9be3e4dc8..664a4f22ff3f 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2836,7 +2836,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer * parameters of the current class are also defined. */ def implementDeferredGivens(body: List[Tree]): List[Tree] = - if cls.is(Trait) then body + if cls.is(Trait) || ctx.isAfterTyper then body else def isGivenValue(mbr: TermRef) = val dcl = mbr.symbol From 1f109738af08dfedd4c8265ee94a4d6b61ba9818 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 8 Jan 2024 15:46:08 +0100 Subject: [PATCH 37/63] Update SemanticDB expects to account for named context bound evidence parameters --- tests/semanticdb/expect/Methods.expect.scala | 2 +- tests/semanticdb/expect/Synthetic.expect.scala | 2 +- tests/semanticdb/metac.expect | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/semanticdb/expect/Methods.expect.scala b/tests/semanticdb/expect/Methods.expect.scala index f34c657b2f6d..4ec723ad584e 100644 --- a/tests/semanticdb/expect/Methods.expect.scala +++ b/tests/semanticdb/expect/Methods.expect.scala @@ -15,7 +15,7 @@ class Methods/*<-example::Methods#*/[T/*<-example::Methods#[T]*/] { def m6/*<-example::Methods#m6().*/(x/*<-example::Methods#m6().(x)*/: Int/*->scala::Int#*/) = ???/*->scala::Predef.`???`().*/ def m6/*<-example::Methods#m6(+1).*/(x/*<-example::Methods#m6(+1).(x)*/: List/*->example::Methods#List#*/[T/*->example::Methods#[T]*/]) = ???/*->scala::Predef.`???`().*/ def m6/*<-example::Methods#m6(+2).*/(x/*<-example::Methods#m6(+2).(x)*/: scala.List/*->scala::package.List#*/[T/*->example::Methods#[T]*/]) = ???/*->scala::Predef.`???`().*/ - def m7/*<-example::Methods#m7().*/[U/*<-example::Methods#m7().[U]*//*<-example::Methods#m7().(evidence$1)*/: Ordering/*->scala::math::Ordering#*/](c/*<-example::Methods#m7().(c)*/: Methods/*->example::Methods#*/[T/*->example::Methods#[T]*/], l/*<-example::Methods#m7().(l)*/: List/*->example::Methods#List#*/[U/*->example::Methods#m7().[U]*/]) = ???/*->scala::Predef.`???`().*/ + def m7/*<-example::Methods#m7().*/[U/*<-example::Methods#m7().[U]*/: Ordering/*->example::Methods#m7().[U]*//*<-example::Methods#m7().(evidence$1)*/](c/*<-example::Methods#m7().(c)*/: Methods/*->example::Methods#*/[T/*->example::Methods#[T]*/], l/*<-example::Methods#m7().(l)*/: List/*->example::Methods#List#*/[U/*->example::Methods#m7().[U]*/]) = ???/*->scala::Predef.`???`().*/ def `m8()./*<-example::Methods#`m8().`().*/`() = ???/*->scala::Predef.`???`().*/ class `m9()./*<-example::Methods#`m9().`#*/` def m9/*<-example::Methods#m9().*/(x/*<-example::Methods#m9().(x)*/: `m9().`/*->example::Methods#`m9().`#*/) = ???/*->scala::Predef.`???`().*/ diff --git a/tests/semanticdb/expect/Synthetic.expect.scala b/tests/semanticdb/expect/Synthetic.expect.scala index a4419aa8bd82..4d797ce2b856 100644 --- a/tests/semanticdb/expect/Synthetic.expect.scala +++ b/tests/semanticdb/expect/Synthetic.expect.scala @@ -30,7 +30,7 @@ class Synthetic/*<-example::Synthetic#*/ { null.asInstanceOf/*->scala::Any#asInstanceOf().*/[Int/*->scala::Int#*/ => Int/*->scala::Int#*/](2) } - class J/*<-example::Synthetic#J#*/[T/*<-example::Synthetic#J#[T]*//*<-example::Synthetic#J#evidence$1.*/: Manifest/*->scala::Predef.Manifest#*/] { val arr/*<-example::Synthetic#J#arr.*/ = Array/*->scala::Array.*/.empty/*->scala::Array.empty().*/[T/*->example::Synthetic#J#[T]*/] } + class J/*<-example::Synthetic#J#*/[T/*<-example::Synthetic#J#[T]*/: /*<-example::Synthetic#J#evidence$1.*/Manifest/*->scala::Predef.Manifest#*//*->example::Synthetic#J#[T]*/] { val arr/*<-example::Synthetic#J#arr.*/ = Array/*->scala::Array.*/.empty/*->scala::Array.empty().*/[T/*->example::Synthetic#J#[T]*/] } class F/*<-example::Synthetic#F#*/ implicit val ordering/*<-example::Synthetic#ordering.*/: Ordering/*->scala::package.Ordering#*/[F/*->example::Synthetic#F#*/] = ???/*->scala::Predef.`???`().*/ diff --git a/tests/semanticdb/metac.expect b/tests/semanticdb/metac.expect index 2120cc633da8..84c3e7c6a110 100644 --- a/tests/semanticdb/metac.expect +++ b/tests/semanticdb/metac.expect @@ -2732,8 +2732,8 @@ Occurrences: [16:29..16:32): ??? -> scala/Predef.`???`(). [17:6..17:8): m7 <- example/Methods#m7(). [17:9..17:10): U <- example/Methods#m7().[U] -[17:10..17:10): <- example/Methods#m7().(evidence$1) -[17:12..17:20): Ordering -> scala/math/Ordering# +[17:12..17:20): Ordering -> example/Methods#m7().[U] +[17:12..17:12): <- example/Methods#m7().(evidence$1) [17:22..17:23): c <- example/Methods#m7().(c) [17:25..17:32): Methods -> example/Methods# [17:33..17:34): T -> example/Methods#[T] @@ -3533,7 +3533,7 @@ Uri => Synthetic.scala Text => empty Language => Scala Symbols => 52 entries -Occurrences => 136 entries +Occurrences => 137 entries Synthetics => 39 entries Symbols: @@ -3659,8 +3659,9 @@ Occurrences: [32:8..32:9): J <- example/Synthetic#J# [32:9..32:9): <- example/Synthetic#J#``(). [32:10..32:11): T <- example/Synthetic#J#[T] -[32:11..32:11): <- example/Synthetic#J#evidence$1. +[32:13..32:13): <- example/Synthetic#J#evidence$1. [32:13..32:21): Manifest -> scala/Predef.Manifest# +[32:13..32:21): Manifest -> example/Synthetic#J#[T] [32:29..32:32): arr <- example/Synthetic#J#arr. [32:35..32:40): Array -> scala/Array. [32:41..32:46): empty -> scala/Array.empty(). From a516cf6c97dac119b95553a00722453ff8152387 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 8 Jan 2024 17:32:43 +0100 Subject: [PATCH 38/63] Fix typos --- .../reference/experimental/typeclasses.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/_docs/reference/experimental/typeclasses.md b/docs/_docs/reference/experimental/typeclasses.md index d41df048f267..cf318894e1d6 100644 --- a/docs/_docs/reference/experimental/typeclasses.md +++ b/docs/_docs/reference/experimental/typeclasses.md @@ -15,7 +15,7 @@ A type class in Scala is a pattern where we define - given instances at specific instantiations of that trait, - using clauses or context bounds abstracting over that trait. -Type classes as a pattern work overall OK, but if we compare them to native implementations in Haskell, or protocols in Swift, or traits in Rust, then there are some idiosyncracies and rough corners which in the end make them +Type classes as a pattern work overall OK, but if we compare them to native implementations in Haskell, or protocols in Swift, or traits in Rust, then there are some idiosyncrasies and rough corners which in the end make them a bit cumbersome and limiting for standard generic programming patterns. Much has improved since Scala 2's implicits, but there is still some gap to bridge to get to parity with these languages. This note shows that with some fairly small and reasonable tweaks to Scala's syntax and typing rules we can obtain a much better scheme for working with type classes, or do generic programming in general. @@ -67,7 +67,7 @@ requires that `Ordering` is a trait or class with a single type parameter (which def flatMap[B](f: A => Self[B]): Self[B] def map[B](f: A => B) = x.flatMap(f `andThen` pure) - def reduce[A: Monoid](xs: List[A] = + def reduce[A: Monoid](xs: List[A]): A = xs.foldLeft(Monoid.unit)(_ `combine` _) trait ParserCombinator: @@ -86,7 +86,7 @@ requires that `Ordering` is a trait or class with a single type parameter (which - Gives a clear indication of traits intended as type classes. A trait is a type class if it has type `Self` as a member - Allows to create aggregate type classes that combine givens via intersection types. - - Allows to use refinements in context bounds (the `combine` example above would be very awkward to express using the old way of context bounds expanding to type copnstructors). + - Allows to use refinements in context bounds (the `combine` example above would be very awkward to express using the old way of context bounds expanding to type constructors). `Self`-based context bounds are a better fit for a dependently typed language like Scala than parameter-based ones. The main reason is that we are dealing with proper types, not type constructors. Proper types can be parameterized, intersected, or refined. This makes `Self`-based designs inherently more compositional than parameterized ones. @@ -120,7 +120,7 @@ and not to: Why not use `This` for the self type? The name `This` suggests that it is the type of `this`. But this is not true for type class traits. `Self` is the name of the type implementing a distinguished _member type_ of the trait in a `given` definition. `Self` is an established term in both Rust and Swift with the meaning used here. - One possible objection to the `Self` based design is that it does not cover "multi-parameter" type classes. But neither do context bounds! "Multi-parameter" type classes in Scala are simply givens that can be synthesized with the standard mechanisms. Type classes in the strict sense abstract only over a single type, namely the implementatation type of a trait. + One possible objection to the `Self` based design is that it does not cover "multi-parameter" type classes. But neither do context bounds! "Multi-parameter" type classes in Scala are simply givens that can be synthesized with the standard mechanisms. Type classes in the strict sense abstract only over a single type, namely the implementation type of a trait. ## Auxiliary Type Alias `is` @@ -383,7 +383,7 @@ Here are some standard type classes, which were mostly already introduced at the def unit: Self trait Functor: - type Self[A] + type Self[A] // Here, Self is a type constructor with parameter A extension [A](x: Self[A]) def map[B](f: A => B): Self[B] trait Monad extends Functor: @@ -547,7 +547,7 @@ given [A: Combinator, B: Combinator { type Input = A.Input }] y <- self.b.parse(in) yield (x, y) ``` -The example is now as expressed as straighforwardly as it should be: +The example is now as expressed as straightforwardly as it should be: - `Combinator` is a type class with two associated types, `Input` and `Result`, and a `parse` method. - `Apply` and `Combine` are two data constructors representing parser combinators. They are declared to be `Combinators` in the two subsequent `given` declarations. @@ -567,12 +567,12 @@ _Note 2:_ One could improve the notation even further by adding equality constra given [A: Combinator, B: Combinator with A.Input == B.Input] => Combine[A, B] is Combinator: ``` -This variant is esthetically pleasing since it makes the equality constraint symmetric. The original version had to use an asymmetric refinement on the second type parameter bound instead. For now, such constraints are neither implemented nor proposed. This is left as a possibility for future work. Note also the analogy with +This variant is aesthetically pleasing since it makes the equality constraint symmetric. The original version had to use an asymmetric refinement on the second type parameter bound instead. For now, such constraints are neither implemented nor proposed. This is left as a possibility for future work. Note also the analogy with the work of @mbovel and @Sporarum on refinement types, where similar `with` clauses can appear for term parameters. If that work goes ahead, we could possibly revisit the issue of `with` clauses also for type parameters. ### Example 4 -Dimi Recordon tried to [port some core elenents](https://github.com/kyouko-taiga/scala-hylolib) of the type class based [Hylo standard library to Scala](https://github.com/hylo-lang/hylo/tree/main/StandardLibrary/Sources). It worked to some degree, but there were some things that could not be expressed, and more things that could be expressed only awkwardly. +Dimi Racordon tried to [port some core elements](https://github.com/kyouko-taiga/scala-hylolib) of the type class based [Hylo standard library to Scala](https://github.com/hylo-lang/hylo/tree/main/StandardLibrary/Sources). It worked to some degree, but there were some things that could not be expressed, and more things that could be expressed only awkwardly. With the improvements proposed here, the library can now be expressed quite clearly and straightforwardly. See tests/pos/hylolib in this PR for details. @@ -636,7 +636,7 @@ These would replace the previous syntax using `@`: val xs @ (x :: xs1) = ys.checkedCast ``` -**Advantages:** No unpronouncible and non-standard symbol like `@`. More regularity. +**Advantages:** No unpronounceable and non-standard symbol like `@`. More regularity. Generally, we want to use `as name` to attach a name for some entity that could also have been used stand-alone. @@ -668,5 +668,5 @@ type classes in Haskell, or with traits in Rust, or with protocols in Swift, or The proposed scheme has similar expressiveness to Protocols in Swift or Traits in Rust. Both of these were largely influenced by Jeremy Siek's PdD thesis "[A language for generic programming](https://scholarworks.iu.edu/dspace/handle/2022/7067)", which was first proposed as a way to implement concepts in C++. C++ did not follow Siek's approach, but Swift and Rust did. In Siek's thesis and in the formal treatments of Rust and Swift, - type class concepts are explained by mapping them to a lower level language of explicit dictionaries with representants for terms and types. Crucially, that lower level is not expressible without loss of granularity in the source language itself, since type representants are mapped to term dictionaries. By contrast, the current proposal expands type class concepts into other well-typed Scala constructs, which ultimately map into well-typed DOT programs. Type classes are simply a convenient notation for something that can already be expressed in Scala. In that sense, we stay true to the philosophy of a _scalable language_, where a small core can support a large range of advanced use cases. + type class concepts are explained by mapping them to a lower level language of explicit dictionaries with representations for terms and types. Crucially, that lower level is not expressible without loss of granularity in the source language itself, since type representations are mapped to term dictionaries. By contrast, the current proposal expands type class concepts into other well-typed Scala constructs, which ultimately map into well-typed DOT programs. Type classes are simply a convenient notation for something that can already be expressed in Scala. In that sense, we stay true to the philosophy of a _scalable language_, where a small core can support a large range of advanced use cases. From a0820a7247d1b515c0dcaee1fbc302101c7dd878 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 8 Jan 2024 19:32:16 +0100 Subject: [PATCH 39/63] Update docs/_docs/reference/experimental/typeclasses.md Co-authored-by: Dale Wijnand --- docs/_docs/reference/experimental/typeclasses.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/reference/experimental/typeclasses.md b/docs/_docs/reference/experimental/typeclasses.md index cf318894e1d6..19484b409ed9 100644 --- a/docs/_docs/reference/experimental/typeclasses.md +++ b/docs/_docs/reference/experimental/typeclasses.md @@ -155,7 +155,7 @@ Context bounds are a convenient and legible abbreviation. A problem so far is th **Proposal:** Allow to name a context bound, like this: ```scala - def reduce[M: Monoid as m](xs: List[M] = + def reduce[M: Monoid as m](xs: List[M]): A = xs.foldLeft(m.unit)(_ `combine` _) ``` From 58d20499a66ace87a6ca600a02b237a9aed0e28a Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 9 Jan 2024 10:13:20 +0100 Subject: [PATCH 40/63] Update syntax changes section in doc page --- docs/_docs/reference/experimental/typeclasses.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/_docs/reference/experimental/typeclasses.md b/docs/_docs/reference/experimental/typeclasses.md index 19484b409ed9..c855fde01994 100644 --- a/docs/_docs/reference/experimental/typeclasses.md +++ b/docs/_docs/reference/experimental/typeclasses.md @@ -344,7 +344,9 @@ GivenSig ::= GivenType ['as' id] ([‘=’ Expr] | TemplateBody) | ConstrApps ['as' id] TemplateBody GivenType ::= AnnotType {id [nl] AnnotType} -TypeParamBounds ::= TypeBounds [‘:’ ContextBounds] +TypeDef ::= id [TypeParamClause] {FunParamClause} TypeAndCtxBounds +TypeParamBounds ::= TypeAndCtxBounds +TypeAndCtxBounds ::= TypeBounds [‘:’ ContextBounds] ContextBounds ::= ContextBound | '{' ContextBound {',' ContextBound} '}' ContextBound ::= Type ['as' id] ``` From f074b391326c9f7914c5433064e581786f1b1a89 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 11 Jan 2024 20:13:16 +0100 Subject: [PATCH 41/63] Update docs/_docs/reference/experimental/typeclasses.md Co-authored-by: Dale Wijnand --- docs/_docs/reference/experimental/typeclasses.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/reference/experimental/typeclasses.md b/docs/_docs/reference/experimental/typeclasses.md index c855fde01994..2d5ad256f40b 100644 --- a/docs/_docs/reference/experimental/typeclasses.md +++ b/docs/_docs/reference/experimental/typeclasses.md @@ -113,7 +113,7 @@ should resolve to: ``` and not to: ```scala -[S](using Sequential[S] +[S](using Sequential[S]) ``` **Discussion** From 330f2d618f28ce2aacfb5e2f821efea9b2e5b362 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 14 Jan 2024 11:55:03 +0100 Subject: [PATCH 42/63] Rename `is` to `forms` This is a trial balloon to see whether `forms` works better than `this`. My immediate reaction is meh. Sometimes it's OK, at other times I liked `is` better. But I admit there's bias since the examples were chosen to work well with `is`. --- .../reference/experimental/typeclasses.md | 42 +++++++++---------- .../scala/runtime/stdLibPatches/Predef.scala | 2 +- tests/pos/FromString.scala | 4 +- tests/pos/deferredSummon.scala | 8 ++-- tests/pos/hylolib-extract.scala | 4 +- tests/pos/hylolib/AnyCollection.scala | 2 +- tests/pos/hylolib/AnyValue.scala | 2 +- tests/pos/hylolib/BitArray.scala | 6 +-- tests/pos/hylolib/HyArray.scala | 6 +-- tests/pos/hylolib/Integers.scala | 8 ++-- tests/pos/hylolib/Slice.scala | 4 +- tests/pos/i10929-new-syntax.scala | 4 +- tests/pos/parsercombinators-arrow.scala | 4 +- tests/pos/parsercombinators-new-syntax.scala | 4 +- tests/pos/typeclass-aggregates.scala | 2 +- tests/pos/typeclasses-arrow.scala | 12 +++--- tests/pos/typeclasses-this.scala | 12 +++--- tests/pos/typeclasses.scala | 18 ++++---- 18 files changed, 72 insertions(+), 72 deletions(-) diff --git a/docs/_docs/reference/experimental/typeclasses.md b/docs/_docs/reference/experimental/typeclasses.md index 2d5ad256f40b..3df98d9226f8 100644 --- a/docs/_docs/reference/experimental/typeclasses.md +++ b/docs/_docs/reference/experimental/typeclasses.md @@ -134,11 +134,11 @@ We introduce a standard type alias `is` in the Scala package or in `Predef`, def This makes writing instance definitions quite pleasant. Examples: ```scala - given Int is Ord ... - given Int is Monoid ... + given Int forms Ord ... + given Int forms Monoid ... type Reader = [X] =>> Env => X - given Reader is Monad ... + given Reader forms Monad ... ``` (more examples will follow below) @@ -149,7 +149,7 @@ This makes writing instance definitions quite pleasant. Examples: Context bounds are a convenient and legible abbreviation. A problem so far is that they are always anonymous, one cannot name the using parameter to which a context bound expands. For instance, without the trick of defining a universal "trampoline" `unit` in the `Monoid` companion object, we would have to write `reduce` like this: ```scala - def reduce[A](xs: List[A])(using m: A is Monoid) = + def reduce[A](xs: List[A])(using m: A forms Monoid) = xs.foldLeft(m.unit)(_ `combine` _) ``` @@ -197,7 +197,7 @@ Context bounds are currently translated to implicit parameters in the last param ``` With the current translation, this would give ```scala - def f[C](x: C.input)(using C: C is ParserCombinator) + def f[C](x: C.input)(using C: C forms ParserCombinator) ``` But this is ill-typed, since the `C` in `C.input` refers to the `C` introduced in the using clause, which comes later. @@ -254,7 +254,7 @@ type T: C in a trait will then expand to ```scala type T -given T is C = deferred +given T forms C = deferred ``` @@ -281,14 +281,14 @@ The awkwardness of the given syntax was forced upon us since we insisted that gi and we can use a more intuitive syntax for givens like this: ```scala -given Int is Ord: +given Int forms Ord: def compare(x: A, y: A) = ... -given [A: Ord] => List[A] is Ord: +given [A: Ord] => List[A] forms Ord: def compare(x: A, y: A) = ... -given Int is Monoid as intMonoid: +given Int forms Monoid as intMonoid: extension (x: Int) def combine(y: Int) = x + y def unit = 0 ``` @@ -303,7 +303,7 @@ The underlying principles are: - an implementation which consists of either an `=` and an expression, or a template body. - - We get the pleasing ` is ` syntax simply by using the predefined infix type `is`. + - We get the pleasing ` forms ` syntax simply by using the predefined infix type `forms`. - Since there is no more middle `:` separating name and parameters from the implemented type, we can use a `:` to start the class body without looking unnatural. That eliminates the special case where `with` was used before. This will be a fairly significant change to the given syntax. I believe there's still a possibility to do this, @@ -326,7 +326,7 @@ This is less of a disruption than it might appear at first: - `given T` was illegal before since abstract givens could not be anonymous. It now means a concrete given of class `T` with no member definitions. This is the natural interpretation for simple tagging given clauses such as - `given String is Value`. + `given String forms Value`. - `given x: T` is legacy syntax for a deferred given. - `given T as x = deferred` is the analogous new syntax, which is more powerful since it allows for automatic instantiation. @@ -396,14 +396,14 @@ Here are some standard type classes, which were mostly already introduced at the // Instances - given Int is Ord: + given Int forms Ord: extension (x: Int) def compareTo(y: Int) = if x < y then -1 else if x > y then +1 else 0 - given [T: Ord] => List[T] is Ord: + given [T: Ord] => List[T] forms Ord: extension (xs: List[T]) def compareTo(ys: List[T]): Int = (xs, ys) match case (Nil, Nil) => 0 @@ -413,7 +413,7 @@ Here are some standard type classes, which were mostly already introduced at the val fst = x.compareTo(y) if (fst != 0) fst else xs1.compareTo(ys1) - given List is Monad: + given List forms Monad: extension [A](xs: List[A]) def flatMap[B](f: A => List[B]): List[B] = xs.flatMap(f) @@ -422,7 +422,7 @@ Here are some standard type classes, which were mostly already introduced at the type Reader[Ctx] = [X] =>> Ctx => X - given [Ctx] => Reader[Ctx] is Monad: + given [Ctx] => Reader[Ctx] forms Monad: extension [A](r: Ctx => A) def flatMap[B](f: A => Ctx => B): Ctx => B = ctx => f(r(ctx))(ctx) @@ -443,7 +443,7 @@ Here are some standard type classes, which were mostly already introduced at the def maximum[T: Ord](xs: List[T]): T = xs.reduce(_ `max` _) - given [T: Ord] => T is Ord as descending: + given [T: Ord] => T forms Ord as descending: extension (x: T) def compareTo(y: T) = T.compareTo(y)(x) def minimum[T: Ord](xs: List[T]) = @@ -483,11 +483,11 @@ trait TupleOf[+A]: object TupleOf: - given EmptyTuple is TupleOf[Nothing]: + given EmptyTuple forms TupleOf[Nothing]: type Mapped[+A] = EmptyTuple def map[B](x: EmptyTuple)(f: Nothing => B): Mapped[B] = x - given [A, Rest <: Tuple : TupleOf[A]] => A *: Rest is TupleOf[A]: + given [A, Rest <: Tuple : TupleOf[A]] => A *: Rest forms TupleOf[A]: type Mapped[+A] = A *: Rest.Mapped[A] def map[B](x: A *: Rest)(f: A => B): Mapped[B] = f(x.head) *: Rest.map(x.tail)(f) @@ -532,14 +532,14 @@ end Combinator case class Apply[I, R](action: I => Option[R]) case class Combine[A, B](a: A, b: B) -given [I, R] => Apply[I, R] is Combinator: +given [I, R] => Apply[I, R] forms Combinator: type Input = I type Result = R extension (self: Apply[I, R]) def parse(in: I): Option[R] = self.action(in) given [A: Combinator, B: Combinator { type Input = A.Input }] - => Combine[A, B] is Combinator: + => Combine[A, B] forms Combinator: type Input = A.Input type Result = (A.Result, B.Result) extension (self: Combine[A, B]) @@ -567,7 +567,7 @@ to take the original example and show how it can be made to work with the new co _Note 2:_ One could improve the notation even further by adding equality constraints in the style of Swift, which in turn resemble the _sharing constraints_ of SML. A hypothetical syntax applied to the second given would be: ```scala given [A: Combinator, B: Combinator with A.Input == B.Input] - => Combine[A, B] is Combinator: + => Combine[A, B] forms Combinator: ``` This variant is aesthetically pleasing since it makes the equality constraint symmetric. The original version had to use an asymmetric refinement on the second type parameter bound instead. For now, such constraints are neither implemented nor proposed. This is left as a possibility for future work. Note also the analogy with the work of @mbovel and @Sporarum on refinement types, where similar `with` clauses can appear for term parameters. If that work goes ahead, we could possibly revisit the issue of `with` clauses also for type parameters. diff --git a/library/src/scala/runtime/stdLibPatches/Predef.scala b/library/src/scala/runtime/stdLibPatches/Predef.scala index 95079298f9ce..43ec109a120f 100644 --- a/library/src/scala/runtime/stdLibPatches/Predef.scala +++ b/library/src/scala/runtime/stdLibPatches/Predef.scala @@ -71,6 +71,6 @@ object Predef: * * which is what is needed for a context bound `[A: TC]`. */ - infix type is[A <: AnyKind, B <: {type Self <: AnyKind}] = B { type Self = A } + infix type forms[A <: AnyKind, B <: {type Self <: AnyKind}] = B { type Self = A } end Predef diff --git a/tests/pos/FromString.scala b/tests/pos/FromString.scala index 4a99f3df3abc..488e4b7f7404 100644 --- a/tests/pos/FromString.scala +++ b/tests/pos/FromString.scala @@ -4,9 +4,9 @@ trait FromString: type Self def fromString(s: String): Self -given Int is FromString = _.toInt +given Int forms FromString = _.toInt -given Double is FromString = _.toDouble +given Double forms FromString = _.toDouble def add[N: {FromString, Numeric as num}](a: String, b: String): N = num.plus(N.fromString(a), N.fromString(b)) diff --git a/tests/pos/deferredSummon.scala b/tests/pos/deferredSummon.scala index 01a034d3cdba..a37cedaac51b 100644 --- a/tests/pos/deferredSummon.scala +++ b/tests/pos/deferredSummon.scala @@ -5,15 +5,15 @@ trait Ord: trait A: type Elem - given Elem is Ord = deferred - def foo = summon[Elem is Ord] + given Elem forms Ord = deferred + def foo = summon[Elem forms Ord] trait B: type Elem: Ord - def foo = summon[Elem is Ord] + def foo = summon[Elem forms Ord] object Inst: - given Int is Ord: + given Int forms Ord: def less(x: Int, y: Int) = x < y object Test1: diff --git a/tests/pos/hylolib-extract.scala b/tests/pos/hylolib-extract.scala index 846e52f30df6..ea7987dc95a0 100644 --- a/tests/pos/hylolib-extract.scala +++ b/tests/pos/hylolib-extract.scala @@ -14,11 +14,11 @@ trait Collection: class BitArray -given Boolean is Value: +given Boolean forms Value: extension (self: Self) def eq(other: Self): Boolean = self == other -given BitArray is Collection: +given BitArray forms Collection: type Element = Boolean extension [Self: Value](self: Self) diff --git a/tests/pos/hylolib/AnyCollection.scala b/tests/pos/hylolib/AnyCollection.scala index 6c2b835852e6..c1ae21e71122 100644 --- a/tests/pos/hylolib/AnyCollection.scala +++ b/tests/pos/hylolib/AnyCollection.scala @@ -38,7 +38,7 @@ object AnyCollection { } -given [T: Value] => AnyCollection[T] is Collection: +given [T: Value] => AnyCollection[T] forms Collection: type Element = T type Position = AnyValue diff --git a/tests/pos/hylolib/AnyValue.scala b/tests/pos/hylolib/AnyValue.scala index 6844135b646b..27a368cbcd8f 100644 --- a/tests/pos/hylolib/AnyValue.scala +++ b/tests/pos/hylolib/AnyValue.scala @@ -58,7 +58,7 @@ object AnyValue { } -given AnyValue is Value: +given AnyValue forms Value: extension (self: AnyValue) def copy(): AnyValue = self.copy() diff --git a/tests/pos/hylolib/BitArray.scala b/tests/pos/hylolib/BitArray.scala index 6ef406e5ad83..851f7d02e783 100644 --- a/tests/pos/hylolib/BitArray.scala +++ b/tests/pos/hylolib/BitArray.scala @@ -318,7 +318,7 @@ object BitArray { } -given BitArray.Position is Value: +given BitArray.Position forms Value: extension (self: BitArray.Position) @@ -331,7 +331,7 @@ given BitArray.Position is Value: def hashInto(hasher: Hasher): Hasher = self.hashInto(hasher) -given BitArray is Collection: +given BitArray forms Collection: type Element = Boolean type Position = BitArray.Position @@ -353,7 +353,7 @@ given BitArray is Collection: def at(p: BitArray.Position): Boolean = self.at(p) -given BitArray is StringConvertible: +given BitArray forms StringConvertible: extension (self: BitArray) override def description: String = var contents = mutable.StringBuilder() diff --git a/tests/pos/hylolib/HyArray.scala b/tests/pos/hylolib/HyArray.scala index de5e83d3b1a3..52faf3647bc1 100644 --- a/tests/pos/hylolib/HyArray.scala +++ b/tests/pos/hylolib/HyArray.scala @@ -158,7 +158,7 @@ object HyArray { } -given [T: Value] => HyArray[T] is Value: +given [T: Value] => HyArray[T] forms Value: extension (self: HyArray[T]) @@ -171,7 +171,7 @@ given [T: Value] => HyArray[T] is Value: def hashInto(hasher: Hasher): Hasher = self.reduce(hasher)((h, e) => e.hashInto(h)) -given [T: Value] => HyArray[T] is Collection: +given [T: Value] => HyArray[T] forms Collection: type Element = T type Position = Int @@ -192,7 +192,7 @@ given [T: Value] => HyArray[T] is Collection: def at(p: Int) = self.at(p) -given [T: {Value, StringConvertible}] => HyArray[T] is StringConvertible: +given [T: {Value, StringConvertible}] => HyArray[T] forms StringConvertible: extension (self: HyArray[T]) override def description: String = val contents = mutable.StringBuilder() diff --git a/tests/pos/hylolib/Integers.scala b/tests/pos/hylolib/Integers.scala index f7334ae40786..d493729740a9 100644 --- a/tests/pos/hylolib/Integers.scala +++ b/tests/pos/hylolib/Integers.scala @@ -1,6 +1,6 @@ package hylo -given Boolean is Value: +given Boolean forms Value: extension (self: Boolean) @@ -14,7 +14,7 @@ given Boolean is Value: def hashInto(hasher: Hasher): Hasher = hasher.combine(if self then 1 else 0) -given Int is Value: +given Int forms Value: extension (self: Int) @@ -28,7 +28,7 @@ given Int is Value: def hashInto(hasher: Hasher): Hasher = hasher.combine(self) -given Int is Comparable: +given Int forms Comparable: extension (self: Int) @@ -43,4 +43,4 @@ given Int is Comparable: def lt(other: Int): Boolean = self < other -given Int is StringConvertible +given Int forms StringConvertible diff --git a/tests/pos/hylolib/Slice.scala b/tests/pos/hylolib/Slice.scala index d54f855b1041..c0860fadd35c 100644 --- a/tests/pos/hylolib/Slice.scala +++ b/tests/pos/hylolib/Slice.scala @@ -24,7 +24,7 @@ final class Slice[Base: Collection]( } -given [C: Collection] => Slice[C] is Collection: +given [C: Collection] => Slice[C] forms Collection: type Element = C.Element type Position = C.Position @@ -51,7 +51,7 @@ given [C: Collection] => Slice[C] is Collection: def at(p: Position) = self.base.at(p) -given [C: Collection] => C.Slice2 is Collection: +given [C: Collection] => C.Slice2 forms Collection: type Element = C.Element type Position = C.Position diff --git a/tests/pos/i10929-new-syntax.scala b/tests/pos/i10929-new-syntax.scala index 11c5e9313d4c..3771c6c80658 100644 --- a/tests/pos/i10929-new-syntax.scala +++ b/tests/pos/i10929-new-syntax.scala @@ -6,11 +6,11 @@ trait TupleOf[+A]: object TupleOf: - given EmptyTuple is TupleOf[Nothing]: + given EmptyTuple forms TupleOf[Nothing]: type Mapped[+A] = EmptyTuple def map[B](x: EmptyTuple)(f: Nothing => B): Mapped[B] = x - given [A, Rest <: Tuple : TupleOf[A]] => A *: Rest is TupleOf[A]: + given [A, Rest <: Tuple : TupleOf[A]] => A *: Rest forms TupleOf[A]: type Mapped[+A] = A *: Rest.Mapped[A] def map[B](x: A *: Rest)(f: A => B): Mapped[B] = (f(x.head) *: Rest.map(x.tail)(f)) diff --git a/tests/pos/parsercombinators-arrow.scala b/tests/pos/parsercombinators-arrow.scala index f8bec02067e5..774adf12df48 100644 --- a/tests/pos/parsercombinators-arrow.scala +++ b/tests/pos/parsercombinators-arrow.scala @@ -19,14 +19,14 @@ end Combinator final case class Apply[C, E](action: C => Option[E]) final case class Combine[A, B](first: A, second: B) -given [C, E] => Apply[C, E] is Combinator: +given [C, E] => Apply[C, E] forms Combinator: type Context = C type Element = E extension(self: Apply[C, E]) def parse(context: C): Option[E] = self.action(context) given [A: Combinator, B: Combinator { type Context = A.Context }] - => Combine[A, B] is Combinator: + => Combine[A, B] forms Combinator: type Context = A.Context type Element = (A.Element, B.Element) extension(self: Combine[A, B]) diff --git a/tests/pos/parsercombinators-new-syntax.scala b/tests/pos/parsercombinators-new-syntax.scala index f984972b915d..f6813214b1b4 100644 --- a/tests/pos/parsercombinators-new-syntax.scala +++ b/tests/pos/parsercombinators-new-syntax.scala @@ -15,14 +15,14 @@ end Combinator case class Apply[I, R](action: I => Option[R]) case class Combine[A, B](first: A, second: B) -given [I, R] => Apply[I, R] is Combinator: +given [I, R] => Apply[I, R] forms Combinator: type Input = I type Result = R extension (self: Apply[I, R]) def parse(in: I): Option[R] = self.action(in) given [A: Combinator, B: Combinator { type Input = A.Input }] - => Combine[A, B] is Combinator: + => Combine[A, B] forms Combinator: type Input = A.Input type Result = (A.Result, B.Result) extension (self: Combine[A, B]) diff --git a/tests/pos/typeclass-aggregates.scala b/tests/pos/typeclass-aggregates.scala index 5e4551b226b7..82bfa3973d3d 100644 --- a/tests/pos/typeclass-aggregates.scala +++ b/tests/pos/typeclass-aggregates.scala @@ -39,7 +39,7 @@ given intMonoid: (Monoid { type Self = Int }) = ??? val x = summon[Ord & Monoid { type Self = Int}] val y: Int = ??? : x.Self -// given [A, B](using ord: A is Ord, monoid: A is Monoid) => A is Ord & Monoid = +// given [A, B](using ord: A forms Ord, monoid: A forms Monoid) => A forms Ord & Monoid = // new ord.OrdProxy with monoid.MonoidProxy {} given [A](using ord: Ord { type Self = A }, monoid: Monoid { type Self = A}): ((Ord & Monoid) { type Self = A}) = diff --git a/tests/pos/typeclasses-arrow.scala b/tests/pos/typeclasses-arrow.scala index 379365ffa1c5..ebf963334db2 100644 --- a/tests/pos/typeclasses-arrow.scala +++ b/tests/pos/typeclasses-arrow.scala @@ -36,14 +36,14 @@ end Common object Instances extends Common: - given Int is Ord as intOrd: + given Int forms Ord as intOrd: extension (x: Int) def compareTo(y: Int) = if x < y then -1 else if x > y then +1 else 0 - given [T: Ord] => List[T] is Ord: + given [T: Ord] => List[T] forms Ord: extension (xs: List[T]) def compareTo(ys: List[T]): Int = (xs, ys) match case (Nil, Nil) => 0 case (Nil, _) => -1 @@ -52,7 +52,7 @@ object Instances extends Common: val fst = x.compareTo(y) if (fst != 0) fst else xs1.compareTo(ys1) - given List is Monad as listMonad: + given List forms Monad as listMonad: extension [A](xs: List[A]) def flatMap[B](f: A => List[B]): List[B] = xs.flatMap(f) def pure[A](x: A): List[A] = @@ -60,7 +60,7 @@ object Instances extends Common: type Reader[Ctx] = [X] =>> Ctx => X - given [Ctx] => Reader[Ctx] is Monad as readerMonad: + given [Ctx] => Reader[Ctx] forms Monad as readerMonad: extension [A](r: Ctx => A) def flatMap[B](f: A => Ctx => B): Ctx => B = ctx => f(r(ctx))(ctx) def pure[A](x: A): Ctx => A = @@ -82,7 +82,7 @@ object Instances extends Common: def maximum[T: Ord](xs: List[T]): T = xs.reduce(_ `max` _) - given [T: Ord] => T is Ord as descending: + given [T: Ord] => T forms Ord as descending: extension (x: T) def compareTo(y: T) = T.compareTo(y)(x) def minimum[T: Ord](xs: List[T]) = @@ -122,7 +122,7 @@ class Sheep(val name: String): println(s"$name gets a haircut!") isNaked = true -given Sheep is Animal: +given Sheep forms Animal: def apply(name: String) = Sheep(name) extension (self: Self) def name: String = self.name diff --git a/tests/pos/typeclasses-this.scala b/tests/pos/typeclasses-this.scala index 20ce78678b22..f6d3872a8c2e 100644 --- a/tests/pos/typeclasses-this.scala +++ b/tests/pos/typeclasses-this.scala @@ -36,7 +36,7 @@ end Common object Instances extends Common: - given intOrd: Int is Ord with + given intOrd: Int forms Ord with extension (x: Int) def compareTo(y: Int) = if x < y then -1 @@ -44,7 +44,7 @@ object Instances extends Common: else 0 // given [T](using tracked val ev: Ord { type Self = T}): Ord { type Self = List[T] } with - given [T: Ord]: List[T] is Ord with + given [T: Ord]: List[T] forms Ord with extension (xs: List[T]) def compareTo(ys: List[T]): Int = (xs, ys) match case (Nil, Nil) => 0 case (Nil, _) => -1 @@ -53,7 +53,7 @@ object Instances extends Common: val fst = x.compareTo(y) if (fst != 0) fst else xs1.compareTo(ys1) - given listMonad: List is Monad with + given listMonad: List forms Monad with extension [A](xs: List[A]) def flatMap[B](f: A => List[B]): List[B] = xs.flatMap(f) def pure[A](x: A): List[A] = @@ -61,7 +61,7 @@ object Instances extends Common: type Reader[Ctx] = [X] =>> Ctx => X - given readerMonad[Ctx]: Reader[Ctx] is Monad with + given readerMonad[Ctx]: Reader[Ctx] forms Monad with extension [A](r: Ctx => A) def flatMap[B](f: A => Ctx => B): Ctx => B = ctx => f(r(ctx))(ctx) def pure[A](x: A): Ctx => A = @@ -83,7 +83,7 @@ object Instances extends Common: def maximum[T: Ord](xs: List[T]): T = xs.reduce(_ `max` _) - given descending[T: Ord]: T is Ord with + given descending[T: Ord]: T forms Ord with extension (x: T) def compareTo(y: T) = T.compareTo(y)(x) def minimum[T: Ord](xs: List[T]) = @@ -123,7 +123,7 @@ class Sheep(val name: String): println(s"$name gets a haircut!") isNaked = true -given Sheep is Animal with +given Sheep forms Animal with def apply(name: String) = Sheep(name) extension (self: Self) def name: String = self.name diff --git a/tests/pos/typeclasses.scala b/tests/pos/typeclasses.scala index d0315a318310..b61edd09cad1 100644 --- a/tests/pos/typeclasses.scala +++ b/tests/pos/typeclasses.scala @@ -31,7 +31,7 @@ end Common object Instances extends Common: - given intOrd: (Int is Ord) with + given intOrd: (Int forms Ord) with type Self = Int extension (x: Int) def compareTo(y: Int) = @@ -39,7 +39,7 @@ object Instances extends Common: else if x > y then +1 else 0 - given listOrd[T](using ord: T is Ord): (List[T] is Ord) with + given listOrd[T](using ord: T forms Ord): (List[T] forms Ord) with extension (xs: List[T]) def compareTo(ys: List[T]): Int = (xs, ys) match case (Nil, Nil) => 0 case (Nil, _) => -1 @@ -49,7 +49,7 @@ object Instances extends Common: if (fst != 0) fst else xs1.compareTo(ys1) end listOrd - given listMonad: (List is Monad) with + given listMonad: (List forms Monad) with extension [A](xs: List[A]) def flatMap[B](f: A => List[B]): List[B] = xs.flatMap(f) def pure[A](x: A): List[A] = @@ -58,9 +58,9 @@ object Instances extends Common: type Reader[Ctx] = [X] =>> Ctx => X - //given [Ctx] => Reader[Ctx] is Monad as readerMonad: + //given [Ctx] => Reader[Ctx] forms Monad as readerMonad: - given readerMonad[Ctx]: (Reader[Ctx] is Monad) with + given readerMonad[Ctx]: (Reader[Ctx] forms Monad) with extension [A](r: Ctx => A) def flatMap[B](f: A => Ctx => B): Ctx => B = ctx => f(r(ctx))(ctx) def pure[A](x: A): Ctx => A = @@ -79,13 +79,13 @@ object Instances extends Common: def flatten: m.Self[A] = xss.flatMap(identity) - def maximum[T](xs: List[T])(using T is Ord): T = + def maximum[T](xs: List[T])(using T forms Ord): T = xs.reduceLeft((x, y) => if (x < y) y else x) - def descending[T](using asc: T is Ord): T is Ord = new: + def descending[T](using asc: T forms Ord): T forms Ord = new: extension (x: T) def compareTo(y: T) = asc.compareTo(y)(x) - def minimum[T](xs: List[T])(using T is Ord) = + def minimum[T](xs: List[T])(using T forms Ord) = maximum(xs)(using descending) def test(): Unit = @@ -132,7 +132,7 @@ instance Sheep: Animal with */ // Implement the `Animal` trait for `Sheep`. -given (Sheep is Animal) with +given (Sheep forms Animal) with def apply(name: String) = Sheep(name) extension (self: Self) def name: String = self.name From af3fd3fedc387dd26567f7f19699791ddb0cbed0 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 14 Jan 2024 11:55:03 +0100 Subject: [PATCH 43/63] Rename `is` to `forms` This is a trial balloon to see whether `forms` works better than `this`. My immediate reaction is meh. Sometimes it's OK, at other times I liked `is` better. But I admit there's bias since the examples were chosen to work well with `is`. --- tests/pos/hylolib/Collection.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/pos/hylolib/Collection.scala b/tests/pos/hylolib/Collection.scala index bef86a967e6e..ccc3b6d6a16e 100644 --- a/tests/pos/hylolib/Collection.scala +++ b/tests/pos/hylolib/Collection.scala @@ -213,7 +213,7 @@ extension [Self: Collection](self: Self) * @complexity * O(n) where n is the number of elements in `self`. */ - def minElement()(using Self.Element is Comparable): Option[Self.Element] = + def minElement()(using Self.Element forms Comparable): Option[Self.Element] = self.minElement(isLessThan = _ `lt` _) /** Returns the maximum element in `self`, using `isGreaterThan` to compare elements. @@ -229,7 +229,7 @@ extension [Self: Collection](self: Self) * @complexity * O(n) where n is the number of elements in `self`. */ - def maxElement()(using Self.Element is Comparable): Option[Self.Element] = + def maxElement()(using Self.Element forms Comparable): Option[Self.Element] = self.maxElement(isGreaterThan = _ `gt` _) /** Returns the maximum element in `self`, using `isOrderedBefore` to compare elements. From c9781faad93a0e7572ec03d34dafe5f3d1e5be15 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 28 Jan 2024 15:42:32 +0100 Subject: [PATCH 44/63] Avoid overwriting type var instance in instantiateWith This is a trial as a first step for other refactorings down the line. --- .../src/dotty/tools/dotc/core/Types.scala | 30 +++++++++---------- tests/neg/showtest.scala | 10 +++++++ tests/pos/showtest.scala | 13 ++++++++ 3 files changed, 37 insertions(+), 16 deletions(-) create mode 100644 tests/neg/showtest.scala create mode 100644 tests/pos/showtest.scala diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index eed9af859296..300b762569db 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4919,20 +4919,21 @@ object Types extends TypeUtils { def isInstantiated(using Context): Boolean = instanceOpt.exists /** Instantiate variable with given type */ - def instantiateWith(tp: Type)(using Context): Type = { - assert(tp ne this, i"self instantiation of $origin, constraint = ${ctx.typerState.constraint}") - assert(!myInst.exists, i"$origin is already instantiated to $myInst but we attempted to instantiate it to $tp") - typr.println(i"instantiating $this with $tp") + def instantiateWith(tp: Type)(using Context): Type = + if myInst.exists then myInst + else + assert(tp ne this, i"self instantiation of $origin, constraint = ${ctx.typerState.constraint}") + assert(!myInst.exists, i"$origin is already instantiated to $myInst but we attempted to instantiate it to $tp") + typr.println(i"instantiating $this with $tp") - if Config.checkConstraintsSatisfiable then - assert(currentEntry.bounds.contains(tp), - i"$origin is constrained to be $currentEntry but attempted to instantiate it to $tp") + if Config.checkConstraintsSatisfiable then + assert(currentEntry.bounds.contains(tp), + i"$origin is constrained to be $currentEntry but attempted to instantiate it to $tp") - if ((ctx.typerState eq owningState.nn.get.uncheckedNN) && !TypeComparer.subtypeCheckInProgress) - setInst(tp) - ctx.typerState.constraint = ctx.typerState.constraint.replace(origin, tp) - tp - } + if ((ctx.typerState eq owningState.nn.get.uncheckedNN) && !TypeComparer.subtypeCheckInProgress) + setInst(tp) + ctx.typerState.constraint = ctx.typerState.constraint.replace(origin, tp) + tp def typeToInstantiateWith(fromBelow: Boolean)(using Context): Type = TypeComparer.instanceType(origin, fromBelow, widenUnions, nestingLevel) @@ -4946,10 +4947,7 @@ object Types extends TypeUtils { */ def instantiate(fromBelow: Boolean)(using Context): Type = val tp = typeToInstantiateWith(fromBelow) - if myInst.exists then // The line above might have triggered instantiation of the current type variable - myInst - else - instantiateWith(tp) + instantiateWith(tp) /** Widen unions when instantiating this variable in the current context? */ def widenUnions(using Context): Boolean = !ctx.typerState.constraint.isHard(this) diff --git a/tests/neg/showtest.scala b/tests/neg/showtest.scala new file mode 100644 index 000000000000..31f3d07d1204 --- /dev/null +++ b/tests/neg/showtest.scala @@ -0,0 +1,10 @@ +trait Show[T]: + extension (x: T) def show: String + +given showAnx: Show[Any] = _.toString + +def print[T: Show](x: T) = println(x.show) + +@main def Test = + println("hello".show) // ok + print("hello".show) // error diff --git a/tests/pos/showtest.scala b/tests/pos/showtest.scala new file mode 100644 index 000000000000..9a54e30c958a --- /dev/null +++ b/tests/pos/showtest.scala @@ -0,0 +1,13 @@ +trait Show[T]: + extension (x: T) def show: String + +given showAny[T]: Show[T] = _.toString +given showInt: Show[Int] = x => s"INT $x" + +def print[T: Show](x: T) = println(x.show) + +@main def Test = + println("hello".show) + println(1.show) + print("hello".show) + print(1) From 59ab35ec9a2cb5a28620722a01037cdcbc68c90a Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 28 Jan 2024 18:11:07 +0100 Subject: [PATCH 45/63] Refactor instantiate This if for better understandability only since it avoids the deeply nested return. Also, add a new test showing how to deal with tracked parameters in typeclass arguments. --- compiler/src/dotty/tools/dotc/core/Types.scala | 3 +-- tests/pos/ord-over-tracked.scala | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 tests/pos/ord-over-tracked.scala diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 300b762569db..997614fdbb4e 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4946,8 +4946,7 @@ object Types extends TypeUtils { * is also a singleton type. */ def instantiate(fromBelow: Boolean)(using Context): Type = - val tp = typeToInstantiateWith(fromBelow) - instantiateWith(tp) + instantiateWith(typeToInstantiateWith(fromBelow)) /** Widen unions when instantiating this variable in the current context? */ def widenUnions(using Context): Boolean = !ctx.typerState.constraint.isHard(this) diff --git a/tests/pos/ord-over-tracked.scala b/tests/pos/ord-over-tracked.scala new file mode 100644 index 000000000000..a9b4aba556e1 --- /dev/null +++ b/tests/pos/ord-over-tracked.scala @@ -0,0 +1,15 @@ +import language.experimental.modularity + +trait Ord[T]: + def lt(x: T, y: T): Boolean + +given Ord[Int] = ??? + +case class D(tracked val x: Int) +given [T <: D]: Ord[T] = (a, b) => a.x < b.x + +def mySort[T: Ord](x: Array[T]): Array[T] = ??? + +def test = + val arr = Array(D(1)) + val arr1 = mySort(arr) // error: no given instance of type Ord[D{val x: (1 : Int)}] \ No newline at end of file From b4cd9baf2217814476b3c3cedd0c88e72f72c984 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 30 Jan 2024 19:52:40 +0100 Subject: [PATCH 46/63] Rename some remaining occurrences of `is` to `forms` --- docs/_docs/reference/experimental/typeclasses.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/_docs/reference/experimental/typeclasses.md b/docs/_docs/reference/experimental/typeclasses.md index 3df98d9226f8..8f5430732369 100644 --- a/docs/_docs/reference/experimental/typeclasses.md +++ b/docs/_docs/reference/experimental/typeclasses.md @@ -123,9 +123,9 @@ and not to: One possible objection to the `Self` based design is that it does not cover "multi-parameter" type classes. But neither do context bounds! "Multi-parameter" type classes in Scala are simply givens that can be synthesized with the standard mechanisms. Type classes in the strict sense abstract only over a single type, namely the implementation type of a trait. -## Auxiliary Type Alias `is` +## Auxiliary Type Alias `forms` -We introduce a standard type alias `is` in the Scala package or in `Predef`, defined like this: +We introduce a standard type alias `forms` in the Scala package or in `Predef`, defined like this: ```scala infix type is[A <: AnyKind, B <: {type Self <: AnyKind}] = B { type Self = A } @@ -656,7 +656,7 @@ I have proposed some tweaks to Scala 3, which would greatly increase its usabili 1. Allow context bounds to be named with `as`. Use the bound parameter name as a default name for the generated context bound evidence. 1. Add a new `{...}` syntax for multiple context bounds. 1. Make context bounds also available for type members, which expand into a new form of deferred given. Phase out the previous abstract givens in favor of the new form. - 1. Add a predefined type alias `is`. + 1. Add a predefined type alias `forms`. 1. Introduce a new cleaner syntax of given clauses. It's interesting that givens, which are a very general concept in Scala, were "almost there" when it comes to full support of concepts and generic programming. We only needed to add a few usability tweaks to context bounds, From 7ae6e8ea8fe4459d57f7e2ddef7eeb325f5b75ad Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 31 Jan 2024 19:02:12 +0100 Subject: [PATCH 47/63] Another test case Shows choice between parameterized and higher-kinded type classes. --- tests/pos/sets-tc.scala | 46 +++++++++++++++++++ .../context-bounds-migration.scala | 4 +- 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 tests/pos/sets-tc.scala rename tests/{neg => warn}/context-bounds-migration.scala (61%) diff --git a/tests/pos/sets-tc.scala b/tests/pos/sets-tc.scala new file mode 100644 index 000000000000..40b3fc968369 --- /dev/null +++ b/tests/pos/sets-tc.scala @@ -0,0 +1,46 @@ +import language.experimental.modularity + +// First version: higher-kinded self type +object v1: + trait Sets: + type Self[A] + def empty[A]: Self[A] + def union[A](self: Self[A], other: Self[A]): Self[A] + + case class ListSet[A](elems: List[A]) + + given ListSet forms Sets: + def empty[A]: ListSet[A] = ListSet(Nil) + + def union[A](self: ListSet[A], other: ListSet[A]): ListSet[A] = + ListSet(self.elems ++ other.elems) + + def listUnion[A, S[_]: Sets](xs: List[S[A]]): S[A] = + xs.foldLeft(S.empty)(S.union) + + val xs = ListSet(List(1, 2, 3)) + val ys = ListSet(List(4, 5)) + val zs = listUnion(List(xs, ys)) + + // Second version: parameterized type class +object v2: + trait Sets[A]: + type Self + def empty: Self + extension (s: Self) def union (other: Self): Self + + case class ListSet[A](elems: List[A]) + + given [A] => ListSet[A] forms Sets[A]: + def empty: ListSet[A] = ListSet(Nil) + + extension (self: ListSet[A]) def union(other: ListSet[A]): ListSet[A] = + ListSet(self.elems ++ other.elems) + + def listUnion[A, S: Sets[A]](xs: List[S]): S = + xs.foldLeft(S.empty)(_ `union` _) + + val xs = ListSet(List(1, 2, 3)) + val ys = ListSet(List(4, 5)) + val zs = listUnion(List(xs, ys)) + diff --git a/tests/neg/context-bounds-migration.scala b/tests/warn/context-bounds-migration.scala similarity index 61% rename from tests/neg/context-bounds-migration.scala rename to tests/warn/context-bounds-migration.scala index b27dc884692c..9d824a919b10 100644 --- a/tests/neg/context-bounds-migration.scala +++ b/tests/warn/context-bounds-migration.scala @@ -1,4 +1,4 @@ -//> using options -Xfatal-warnings + class C[T] def foo[X: C] = () @@ -6,5 +6,5 @@ def foo[X: C] = () given [T]: C[T] = C[T]() def Test = - foo(C[Int]()) // error + foo(C[Int]()) // warning foo(using C[Int]()) // ok From 8203e0c8f78419e5f5a58c89ff8d1c2d04afd659 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 4 Feb 2024 18:25:18 +0100 Subject: [PATCH 48/63] Remember language imports when expanding toplevel symbols --- .../tools/dotc/printing/RefinedPrinter.scala | 2 +- .../dotty/tools/dotc/typer/ImportInfo.scala | 2 -- .../src/dotty/tools/dotc/typer/Namer.scala | 18 ++++++++++++++++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index a593bc6fdda5..0658b4f6d932 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -786,7 +786,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { case CapturesAndResult(refs, parent) => changePrec(GlobalPrec)("^{" ~ Text(refs.map(toText), ", ") ~ "}" ~ toText(parent)) case ContextBoundTypeTree(tycon, pname, ownName) => - toText(pname) ~ " is " ~ toText(tycon) ~ (" as " ~ toText(ownName) `provided` !ownName.isEmpty) + toText(pname) ~ " : " ~ toText(tycon) ~ (" as " ~ toText(ownName) `provided` !ownName.isEmpty) case _ => tree.fallbackToText(this) } diff --git a/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala b/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala index 78cba674bfff..04e6f03565d5 100644 --- a/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala +++ b/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala @@ -180,8 +180,6 @@ class ImportInfo(symf: Context ?=> Symbol, assert(myUnimported != null) myUnimported.uncheckedNN - private val isLanguageImport: Boolean = untpd.languageImport(qualifier).isDefined - private var myUnimported: Symbol | Null = uninitialized private var featureCache: SimpleIdentityMap[TermName, java.lang.Boolean] = SimpleIdentityMap.empty diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 3cc8341f3d5b..b4ab09207dd5 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -296,12 +296,15 @@ class Namer { typer: Typer => createOrRefine[Symbol](tree, name, flags, ctx.owner, _ => info, (fs, _, pwithin) => newSymbol(ctx.owner, name, fs, info, pwithin, tree.nameSpan)) case tree: Import => - recordSym(newImportSymbol(ctx.owner, Completer(tree)(ctx), tree.span), tree) + recordSym(newImportSym(tree), tree) case _ => NoSymbol } } + private def newImportSym(imp: Import)(using Context): Symbol = + newImportSymbol(ctx.owner, Completer(imp)(ctx), imp.span) + /** If `sym` exists, enter it in effective scope. Check that * package members are not entered twice in the same run. */ @@ -706,7 +709,18 @@ class Namer { typer: Typer => enterSymbol(companion) end addAbsentCompanions - stats.foreach(expand) + /** Expand each statement, keeping track of language imports in the context. This is + * necessary since desugaring might depend on language imports. + */ + def expandTopLevel(stats: List[Tree])(using Context): Unit = stats match + case (imp @ Import(qual, _)) :: stats1 if untpd.languageImport(qual).isDefined => + expandTopLevel(stats1)(using ctx.importContext(imp, newImportSym(imp))) + case stat :: stats1 => + expand(stat) + expandTopLevel(stats1) + case Nil => + + expandTopLevel(stats) mergeCompanionDefs() val ctxWithStats = stats.foldLeft(ctx)((ctx, stat) => indexExpanded(stat)(using ctx)) inContext(ctxWithStats) { From a59165a7b8b8c0fdd547aa6ee3091157cd95c570 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 24 Feb 2024 18:34:55 +0100 Subject: [PATCH 49/63] Update MimaFilters --- project/MiMaFilters.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 7565d23b2c1b..529c238c38b7 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -8,6 +8,8 @@ object MiMaFilters { val ForwardsBreakingChanges: Map[String, Seq[ProblemFilter]] = Map( // Additions that require a new minor version of the library Build.previousDottyVersion -> Seq( + ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.modularity"), + ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$modularity$"), ), // Additions since last LTS @@ -71,6 +73,8 @@ object MiMaFilters { val ForwardsBreakingChanges: Map[String, Seq[ProblemFilter]] = Map( // Additions that require a new minor version of tasty core Build.previousDottyVersion -> Seq( + ProblemFilters.exclude[DirectMissingMethodProblem]("dotty.tools.tasty.TastyFormat.TRACKED"), + ProblemFilters.exclude[DirectMissingMethodProblem]("dotty.tools.tasty.TastyFormat.TRACKED"), ), // Additions since last LTS From 102c5ee71437d5f8a3da099b68b69886d7ce2241 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 26 Feb 2024 11:34:24 +0100 Subject: [PATCH 50/63] Fix typo --- docs/_docs/reference/experimental/typeclasses.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/reference/experimental/typeclasses.md b/docs/_docs/reference/experimental/typeclasses.md index 8f5430732369..b7dd8cd5fbc5 100644 --- a/docs/_docs/reference/experimental/typeclasses.md +++ b/docs/_docs/reference/experimental/typeclasses.md @@ -179,7 +179,7 @@ The old syntax with multiple `:` should be phased out over time. So far, a context bound for a type `M` gets a synthesized fresh name. It would be much more useful if it got the name of the constrained type instead, translated to be a term name. This means our `reduce` method over monoids would not even need an `as` binding. We could simply formulate it as follows: ``` - def reduce[M: Monoid](xs: List[M] = + def reduce[M: Monoid](xs: List[M]) = xs.foldLeft(M.unit)(_ `combine` _) ``` From 1a7a60f20c1f987a32e7c0d9163be4cd5c2d4a88 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 27 Feb 2024 11:30:49 +0100 Subject: [PATCH 51/63] Expand deferred givens to lazy abstract givens That way they can be implemented by operations of the form `given T = f(...)`, which also map to lazy values. And strict abstract values cannot be overridden by lazy values since that could undermine realizability and with it type soundness. --- compiler/src/dotty/tools/dotc/typer/Namer.scala | 2 +- tests/pos/deferred-givens.scala | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 tests/pos/deferred-givens.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index b4ab09207dd5..fae7cbef2f35 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -1827,7 +1827,7 @@ class Namer { typer: Typer => if !sym.maybeOwner.is(Trait) then report.error(em"`deferred` can only be used for givens in traits", mdef.rhs.srcPos) else - sym.resetFlag(Final | Lazy) + sym.resetFlag(Final) sym.setFlag(Deferred | HasDefault) case _ => diff --git a/tests/pos/deferred-givens.scala b/tests/pos/deferred-givens.scala new file mode 100644 index 000000000000..ac3b6257e115 --- /dev/null +++ b/tests/pos/deferred-givens.scala @@ -0,0 +1,11 @@ +import compiletime.* +class Ord[Elem] + +trait B: + type Elem + given Ord[Elem] = deferred + def foo = summon[Ord[Elem]] + +class C extends B: + type Elem = String + given Ord[Elem] = ??? From f960914d7533aa5b2e5b45cb203512dc3ee7acee Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 27 Feb 2024 11:44:19 +0100 Subject: [PATCH 52/63] Make deferred a method in the compiletime package --- .../dotty/tools/dotc/core/Definitions.scala | 1 + .../dotty/tools/dotc/transform/Erasure.scala | 8 +++++++- .../src/dotty/tools/dotc/typer/Namer.scala | 12 ++++++------ .../src/dotty/tools/dotc/typer/RefChecks.scala | 2 +- .../src/dotty/tools/dotc/typer/Typer.scala | 18 +++++++++++++++--- library/src/scala/compiletime/package.scala | 13 +++++++++++++ tests/neg/deferred-givens.check | 16 ++++++++-------- tests/neg/deferred-givens.scala | 2 ++ tests/neg/deferredSummon.check | 17 +++++++++++++++++ tests/neg/deferredSummon.scala | 11 +++++++++-- tests/pos/deferred-givens.scala | 15 ++++++++++++++- tests/pos/deferredSummon.scala | 2 ++ 12 files changed, 95 insertions(+), 22 deletions(-) create mode 100644 tests/neg/deferredSummon.check diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 789e744fbfc9..ca32cc0d3433 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -240,6 +240,7 @@ class Definitions { @tu lazy val Compiletime_codeOf: Symbol = CompiletimePackageClass.requiredMethod("codeOf") @tu lazy val Compiletime_erasedValue : Symbol = CompiletimePackageClass.requiredMethod("erasedValue") @tu lazy val Compiletime_uninitialized: Symbol = CompiletimePackageClass.requiredMethod("uninitialized") + @tu lazy val Compiletime_deferred : Symbol = CompiletimePackageClass.requiredMethod("deferred") @tu lazy val Compiletime_error : Symbol = CompiletimePackageClass.requiredMethod(nme.error) @tu lazy val Compiletime_requireConst : Symbol = CompiletimePackageClass.requiredMethod("requireConst") @tu lazy val Compiletime_constValue : Symbol = CompiletimePackageClass.requiredMethod("constValue") diff --git a/compiler/src/dotty/tools/dotc/transform/Erasure.scala b/compiler/src/dotty/tools/dotc/transform/Erasure.scala index 01fc423b0076..3daa32af7695 100644 --- a/compiler/src/dotty/tools/dotc/transform/Erasure.scala +++ b/compiler/src/dotty/tools/dotc/transform/Erasure.scala @@ -567,7 +567,13 @@ object Erasure { case Some(annot) => val message = annot.argumentConstant(0) match case Some(c) => - c.stringValue.toMessage + val addendum = tree match + case tree: RefTree + if tree.symbol == defn.Compiletime_deferred && tree.name != nme.deferred => + i".\nNote that `deferred` can only be used under its own name when implementing a given in a trait; `${tree.name}` is not accepted." + case _ => + "" + (c.stringValue ++ addendum).toMessage case _ => em"""Reference to ${tree.symbol.showLocated} should not have survived, |it should have been processed and eliminated during expansion of an enclosing macro or term erasure.""" diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index fae7cbef2f35..38f4b10e0a69 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -1823,12 +1823,12 @@ class Namer { typer: Typer => // translate `given T = deferred` to an abstract given with HasDefault flag if sym.is(Given) then mdef.rhs match - case Ident(nme.deferred) if Feature.enabled(modularity) => - if !sym.maybeOwner.is(Trait) then - report.error(em"`deferred` can only be used for givens in traits", mdef.rhs.srcPos) - else - sym.resetFlag(Final) - sym.setFlag(Deferred | HasDefault) + case rhs: RefTree + if rhs.name == nme.deferred + && typedAheadExpr(rhs).symbol == defn.Compiletime_deferred + && sym.maybeOwner.is(Trait) => + sym.resetFlag(Final) + sym.setFlag(Deferred | HasDefault) case _ => val mbrTpe = paramFn(checkSimpleKinded(typedAheadType(mdef.tpt, tptProto)).tpe) diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index 4834fc37d29b..12848e26cf26 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -552,7 +552,7 @@ object RefChecks { overrideError("is an extension method, cannot override a normal method") else if (other.is(ExtensionMethod) && !member.is(ExtensionMethod)) // (1.3) overrideError("is a normal method, cannot override an extension method") - else if !other.is(Deferred) + else if (!other.is(Deferred) || other.isAllOf(Given | HasDefault)) && !member.is(Deferred) && !other.name.is(DefaultGetterName) && !member.isAnyOverride diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 664a4f22ff3f..39a82dc78ce2 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2852,20 +2852,32 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val dcl = mbr.symbol val target = dcl.info.asSeenFrom(cls.thisType, dcl.owner) val constr = cls.primaryConstructor - val paramScope = newScopeWith(cls.paramAccessors.filter(_.is(Given))*) + val usingParamAccessors = cls.paramAccessors.filter(_.is(Given)) + val paramScope = newScopeWith(usingParamAccessors*) val searchCtx = ctx.outer.fresh.setScope(paramScope) val rhs = implicitArgTree(target, cdef.span, where = i"inferring the implementation of the deferred ${dcl.showLocated}" )(using searchCtx) + val impl = dcl.copy(cls, - flags = dcl.flags &~ (HasDefault | Deferred) | Final, + flags = dcl.flags &~ (HasDefault | Deferred) | Final | Override, info = target, coord = rhs.span).entered.asTerm - ValDef(impl, rhs) + + def anchorParams = new TreeMap: + override def transform(tree: Tree)(using Context): Tree = tree match + case id: Ident if usingParamAccessors.contains(id.symbol) => + cpy.Select(id)(This(cls), id.name) + case _ => + super.transform(tree) + ValDef(impl, anchorParams.transform(rhs)) + end givenImpl val givenImpls = cls.thisType.implicitMembers + //.showing(i"impl def givens for $cls/$result") .filter(_.symbol.isAllOf(DeferredGivenFlags, butNot = Param)) + //.showing(i"impl def filtered givens for $cls/$result") .filter(isGivenValue) .map(givenImpl) body ++ givenImpls diff --git a/library/src/scala/compiletime/package.scala b/library/src/scala/compiletime/package.scala index 3eca997554a0..be76941a680b 100644 --- a/library/src/scala/compiletime/package.scala +++ b/library/src/scala/compiletime/package.scala @@ -42,6 +42,19 @@ def erasedValue[T]: T = erasedValue[T] @compileTimeOnly("`uninitialized` can only be used as the right hand side of a mutable field definition") def uninitialized: Nothing = ??? +/** Used as the right hand side of a given in a trait, like this + * + * ``` + * given T = deferred + * ``` + * + * This signifies that the given will get a synthesized definition in all classes + * that implement the enclosing trait and that do not contain an explicit overriding + * definition of that given. + */ +@compileTimeOnly("`deferred` can only be used as the right hand side of a given definition in a trait") +def deferred: Nothing = ??? + /** The error method is used to produce user-defined compile errors during inline expansion. * If an inline expansion results in a call error(msgStr) the compiler produces an error message containing the given msgStr. * diff --git a/tests/neg/deferred-givens.check b/tests/neg/deferred-givens.check index 771dd98f40c8..cc15901d087f 100644 --- a/tests/neg/deferred-givens.check +++ b/tests/neg/deferred-givens.check @@ -1,13 +1,13 @@ --- [E172] Type Error: tests/neg/deferred-givens.scala:9:6 -------------------------------------------------------------- -9 |class B extends A // error - |^^^^^^^^^^^^^^^^^ - |No given instance of type Ctx was found for inferring the implementation of the deferred given instance ctx in trait A --- [E172] Type Error: tests/neg/deferred-givens.scala:11:15 ------------------------------------------------------------ -11 |abstract class C extends A // error +-- [E172] Type Error: tests/neg/deferred-givens.scala:11:6 ------------------------------------------------------------- +11 |class B extends A // error + |^^^^^^^^^^^^^^^^^ + |No given instance of type Ctx was found for inferring the implementation of the deferred given instance ctx in trait A +-- [E172] Type Error: tests/neg/deferred-givens.scala:13:15 ------------------------------------------------------------ +13 |abstract class C extends A // error |^^^^^^^^^^^^^^^^^^^^^^^^^^ |No given instance of type Ctx was found for inferring the implementation of the deferred given instance ctx in trait A --- Error: tests/neg/deferred-givens.scala:24:8 ------------------------------------------------------------------------- -24 | class E extends A2 // error, can't summon polymorphic given +-- Error: tests/neg/deferred-givens.scala:26:8 ------------------------------------------------------------------------- +26 | class E extends A2 // error, can't summon polymorphic given | ^^^^^^^^^^^^^^^^^^ | Cannnot infer the implementation of the deferred given instance given_Ctx3_T in trait A2 | since that given is parameterized. An implementing given needs to be written explicitly. diff --git a/tests/neg/deferred-givens.scala b/tests/neg/deferred-givens.scala index 8581f052c663..7ff67d784714 100644 --- a/tests/neg/deferred-givens.scala +++ b/tests/neg/deferred-givens.scala @@ -1,4 +1,6 @@ //> using options -language:experimental.modularity -source future +import compiletime.deferred + class Ctx class Ctx2 diff --git a/tests/neg/deferredSummon.check b/tests/neg/deferredSummon.check new file mode 100644 index 000000000000..bd76ad73467e --- /dev/null +++ b/tests/neg/deferredSummon.check @@ -0,0 +1,17 @@ +-- Error: tests/neg/deferredSummon.scala:4:26 -------------------------------------------------------------------------- +4 | given Int = compiletime.deferred // error + | ^^^^^^^^^^^^^^^^^^^^ + | `deferred` can only be used as the right hand side of a given definition in a trait +-- Error: tests/neg/deferredSummon.scala:7:26 -------------------------------------------------------------------------- +7 | given Int = compiletime.deferred // error + | ^^^^^^^^^^^^^^^^^^^^ + | `deferred` can only be used as the right hand side of a given definition in a trait +-- Error: tests/neg/deferredSummon.scala:12:16 ------------------------------------------------------------------------- +12 | given Int = deferred // error + | ^^^^^^^^ + | `deferred` can only be used as the right hand side of a given definition in a trait +-- Error: tests/neg/deferredSummon.scala:16:14 ------------------------------------------------------------------------- +16 | given Int = defered // error + | ^^^^^^^ + |`deferred` can only be used as the right hand side of a given definition in a trait. + |Note that `deferred` can only be used under its own name when implementing a given in a trait; `defered` is not accepted. diff --git a/tests/neg/deferredSummon.scala b/tests/neg/deferredSummon.scala index e379bee27f7a..cddde82535fb 100644 --- a/tests/neg/deferredSummon.scala +++ b/tests/neg/deferredSummon.scala @@ -1,12 +1,19 @@ //> using options -language:experimental.modularity object Test: - given Int = deferred // error + given Int = compiletime.deferred // error abstract class C: - given Int = deferred // error + given Int = compiletime.deferred // error trait A: + import compiletime.deferred locally: given Int = deferred // error +trait B: + import compiletime.deferred as defered + given Int = defered // error + + + diff --git a/tests/pos/deferred-givens.scala b/tests/pos/deferred-givens.scala index ac3b6257e115..049a6a0860d1 100644 --- a/tests/pos/deferred-givens.scala +++ b/tests/pos/deferred-givens.scala @@ -1,3 +1,4 @@ +//> using options -language:experimental.modularity -source future import compiletime.* class Ord[Elem] @@ -8,4 +9,16 @@ trait B: class C extends B: type Elem = String - given Ord[Elem] = ??? + override given Ord[Elem] = ??? + +def bar(using Ord[String]) = 1 + +class D(using Ord[String]) extends B: + type Elem = String + +class E(using x: Ord[String]) extends B: + type Elem = String + override given Ord[Elem] = x + +class F[X: Ord] extends B: + type Elem = X diff --git a/tests/pos/deferredSummon.scala b/tests/pos/deferredSummon.scala index a37cedaac51b..494b5d84df21 100644 --- a/tests/pos/deferredSummon.scala +++ b/tests/pos/deferredSummon.scala @@ -1,4 +1,6 @@ //> using options -language:experimental.modularity -source future +import compiletime.deferred + trait Ord: type Self def less(x: Self, y: Self): Boolean From ab2341aaa160c94266263ddc9d3d4f7bf2886d5e Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 27 Feb 2024 14:40:07 +0100 Subject: [PATCH 53/63] Update doc page and break out syntax Pre-SIP into separate doc --- .../experimental/typeclasses-syntax.md | 350 ++++++++++++++++++ .../reference/experimental/typeclasses.md | 246 ++++++++---- 2 files changed, 524 insertions(+), 72 deletions(-) create mode 100644 docs/_docs/reference/experimental/typeclasses-syntax.md diff --git a/docs/_docs/reference/experimental/typeclasses-syntax.md b/docs/_docs/reference/experimental/typeclasses-syntax.md new file mode 100644 index 000000000000..bdafe9d6381c --- /dev/null +++ b/docs/_docs/reference/experimental/typeclasses-syntax.md @@ -0,0 +1,350 @@ + +# Pre-SIP: Improve Syntax for Context Bounds and Givens + +Some months ago I experimented with some small extensions and tweaks to Scala that would make Scala's support for type classes much more pleasant. It would put it on a par of what is supported by Rust or Swift but at the same time be distinctly Scala like, with simple and concise syntax and clear semantic foundations. + +That experiment consisted of three areas which are independent of each other: + + 1. Allow context bounds also for types with `Self` type members. + 2. Better support for modularity: keep track of the types of certain class arguments. + 3. Syntactic improvements: Named context bounds, simpler and more regular syntax for givens. + +I would like to propose each of these areas as separate SIPs. That makes them easier to review and discuss. Furthermore, each change set has value independently of what happens to the other proposals. + +I'll start with (3. syntactic improvements) which is the largest chunk of changes and also the most time sensitive. It contains new syntax that supersedes some existing syntax that was introduced in 3.0, so it's better to make the change at a time when not that much code using the new syntax is written yet. +By contrast the other two areas are less urgent. The set of changes proposed here are valuable to have, even if the other two areas are not, or not yet, accepted. + +The proposal is structured in three parts, covering named context bounds, +context bounds for type members, and changes to the given syntax. + +## 1. Named Context Bounds + +Context bounds are a convenient and legible abbreviation. A problem so far is that they are always anonymous, one cannot name the implicit parameter to which a context bound expands. For instance, consider the classical pair of type classes +```scala + trait SemiGroup[A]: + extension (x: A) def combine(y: A): A + + trait Monoid[A] extends SemiGroup[A]: + def unit: A +``` +and a `reduce` method defined like this: +```scala +def reduce[A : Monoid](xs: List[A]): A = ??? +``` +Since we don't have a name for the `Monoid` instance of `A`, we need to resort to `summon` in the body of `reduce`: +```scala +def reduce[A : Monoid](xs: List[A]): A = + xs.foldLeft(summon Monoid[A])(_ `combine` _) +``` +That's generally considered too painful to write and read, hence people usually adopt one of two alternatives. Either, eschew context bounds and switch to using clauses: +```scala +def reduce[A](xs: List[A])(using m: Monoid[A]): A = + xs.foldLeft(m)(_ `combine` _) +``` +Or, plan ahead and define a "trampoline" method in `Monoid`'s companion object: +```scala + trait Monoid[A] extends SemiGroup[A]: + def unit: A + object Monoid: + def unit[A](using m: Monoid[A]): A = m.unit + ... + def reduce[A : Monoid](xs: List[A]): A = + xs.foldLeft(Monoid.unit)(_ `combine` _) +``` +This is all accidental complexity which can be avoided by the following proposal. + +**Proposal:** Allow to name a context bound, like this: +```scala + def reduce[A : Monoid as m](xs: List[A]): A = + xs.foldLeft(m.unit)(_ `combine` _) +``` + +We use `as x` after the type to bind the instance to `x`. This is analogous to import renaming, which also introduces a new name for something that comes before. + +**Benefits:** The new syntax is simple and clear. It avoids the awkward choice between concise context bounds that can't be named and verbose using clauses that can. + +### New Syntax for Aggregate Context Bounds + +Aggregate context bounds like `A : X : Y` are not obvious to read, and it becomes worse when we add names, e.g. `A : X as x : Y as y`. + +**Proposal:** Allow to combine several context bounds inside `{...}`, analogous +to import clauses. Example: + +```scala + trait: + def showMax[X : {Ordering, Show}](x: X, y: X): String + class B extends A: + def showMax[X : {Ordering as ordering, Show as show}](x: X, y: X): String = + show.asString(ordering.max(x, y)) +``` + +The old syntax with multiple `:` should be phased out over time. There's more about migration at the end of this Pre-SIP. + +**Benefits:** The new syntax is much clearer than the old one, in particular for newcomers that don't know context bounds well. + +### Better Default Names for Context Bounds + +So far, an unnamed context bound for a type parameter gets a synthesized fresh name. It would be much more useful if it got the name of the constrained type parameter instead, translated to be a term name. This means our `reduce` method over monoids would not even need an `as` binding. We could simply formulate it as follows: +``` + def reduce[A : Monoid](xs: List[A]) = + xs.foldLeft(A.unit)(_ `combine` _) +``` + +The use of a name like `A` above in two variants, both as a type name and as a term name is of course familiar to Scala programmers. We use the same convention for classes and companion objects. In retrospect, the idea of generalizing this to also cover type parameters is obvious. It is surprising that it was not brought up before. + +**Proposed Rules** + + 1. The generated evidence parameter for a context bound `A : C as a` has name `a` + 2. The generated evidence for a context bound `A : C` without an `as` binding has name `A` (seen as a term name). So, `A : C` is equivalent to `A : C as A`. + 3. If there are more than one context bounds for a type parameter, the generated evidence parameter for every context bound except the first one has a fresh synthesized name, unless the context bound carries an `as` clause, in which case rule (1) applies. + +The default naming convention reduces the need for named context bounds. But named context bounds are still essential, for at least two reasons: + + - They are needed to give names to multiple context bounds. + - They give an explanation what a single unnamed context bound expands to. + + +### Expansion of Context Bounds + +Context bounds are currently translated to implicit parameters in the last parameter list of a method or class. This is a problem if a context bound is mentioned in one of the preceding parameter types. For example, consider a type class of parsers with associated type members `Input` and `Result` describing the input type on which the parsers operate and the type of results they produce: +```scala +trait Parser[P]: + type Input + type Result +``` +Here is a method `run` that runs a parser on an input of the required type: + +```scala +def run[P : Parser](in: P.Input): P.Result +``` +Or, making clearer what happens by using an explicit name for the context bound: +```scala +def run[P : Parser as p](in: p.Input): p.Result +``` +With the current translation this does not work since it would be expanded to: +```scala + def run[P](x: p.Input)(using p: Parser[P]): p.Result +``` +Note that the `p` in `p.Input` refers to the `p` introduced in the using clause, which comes later. So this is ill-formed. + +This problem would be fixed by changing the translation of context bounds so that they expand to using clauses immediately after the type parameter. But such a change is infeasible, for two reasons: + + 1. It would be a binary-incompatible change. + 2. Putting using clauses earlier can impair type inference. A type in + a using clause can be constrained by term arguments coming before that + clause. Moving the using clause first would miss those constraints, which could cause ambiguities in implicit search. + +But there is an alternative which is feasible: + +**Proposal:** Map the context bounds of a method or class as follows: + + 1. If one of the bounds is referred to by its term name in a subsequent parameter clause, the context bounds are mapped to a using clause immediately preceding the first such parameter clause. + 2. Otherwise, if the last parameter clause is a using (or implicit) clause, merge all parameters arising from context bounds in front of that clause, creating a single using clause. + 3. Otherwise, let the parameters arising from context bounds form a new using clause at the end. + +Rules (2) and (3) are the status quo, and match Scala 2's rules. Rule (1) is new but since context bounds so far could not be referred to, it does not apply to legacy code. Therefore, binary compatibility is maintained. + +**Discussion** More refined rules could be envisaged where context bounds are spread over different using clauses so that each comes as late as possible. But it would make matters more complicated and the gain in expressiveness is not clear to me. + + +## 2. Context Bounds for Type Members + +It's not very orthogonal to allow subtype bounds for both type parameters and abstract type members, but context bounds only for type parameters. What's more, we don't even have the fallback of an explicit using clause for type members. The only alternative is to also introduce a set of abstract givens that get implemented in each subclass. This is extremely heavyweight and opaque to newcomers. + +**Proposal**: Allow context bounds for type members. Example: + +```scala + class Collection: + type Element : Ord +``` + +The question is how these bounds are expanded. Context bounds on type parameters +are expanded into using clauses. But for type members this does not work, since we cannot refer to a member type of a class in a parameter type of that class. What we are after is an equivalent of using parameter clauses but represented as class members. + +**Proposal:** ~~Introduce a new kind of given definition of the form:~~ +Introduce a new way to implement a given definition in a trait like this: +```scala +given T = deferred +``` +~~`deferred` is a soft keyword which has special meaning only in this context. +A given with `deferred` right hand side can appear only as a member definition of some trait.~~ +`deferred` is a new method in the `scala.compiletime` package, which can appear only as the right hand side of a given defined in a trait. Any class implementing that trait will provide an implementation of this given. If a definition is not provided explicitly, it will be synthesized by searching for a given of type `T` in the scope of the inheriting class. Specifically, the scope in which this given will be searched is the environment of that class augmented by its parameters but not containing its members (since that would lead to recursive resolutions). If an implementation _is_ provided explicitly, it counts as an override of a concrete definition and needs an `override` modifier. + +Deferred givens allow a clean implementation of context bounds in traits, +as in the following example: +```scala +trait Sorted: + type Element : Ord + +class SortedSet[A : Ord] extends Sorted: + type Element = A +``` +The compiler expands this to the following implementation: +```scala +trait Sorted: + type Element + given Ord[Element] = compiletime.deferred + +class SortedSet[A](using A: Ord[A]) extends Sorted: + type Element = A + override given Ord[Element] = A // i.e. the A defined by the using clause +``` + +The using clause in class `SortedSet` provides an implementation for the deferred given in trait `Sorted`. + +**Benefits:** + + - Better orthogonality, type parameters and abstract type members now accept the same kinds of bounds. + - Better ergonomics, since deferred givens get naturally implemented in inheriting classes, no need for boilerplate to fill in definitions of abstract givens. + +**Alternative:** It was suggested that we use a modifier for a deferred given instead of a `= deferred`. Something like `deferred given C[T]`. But a modifier does not suggest the concept that a deferred given will be implemented automatically in subclasses unless an explicit definition is written. In a sense, we can see `= deferred` as the invocation of a magic macro that is provided by the compiler. So from a user's point of view a given with `deferred` right hand side is not abstract. +It is a concrete definition where the compiler will provide the correct implementation. + +## 3. Cleanup of Given Syntax + +A good language syntax is like a Bach fugue: A small set of motifs is combined in a multitude of harmonic ways. Dissonances and irregularities should be avoided. + +When designing Scala 3, I believe that, by and large, we achieved that goal, except in one area, which is the syntax of givens. There _are_ some glaring dissonances, as seen in this code for defining an ordering on lists: +```scala +given [A](using Ord[A]): Ord[List[A]] with + def compare(x: List[A], y: List[A]) = ... +``` +The `:` feels utterly foreign in this position. It's definitely not a type ascription, so what is its role? Just as bad is the trailing `with`. Everywhere else we use braces or trailing `:` to start a scope of nested definitions, so the need of `with` sticks out like a sore thumb. + +We arrived at that syntax not because of a flight of fancy but because even after trying for about a year to find other solutions it seemed like the least bad alternative. The awkwardness of the given syntax arose because we insisted that givens could be named or anonymous, with the default on anonymous, that we would not use underscore for an anonymous given, and that the name, if present, had to come first, and have the form `name [parameters] :`. In retrospect, that last requirement showed a lack of creativity on our part. + +Sometimes unconventional syntax grows on you and becomes natural after a while. But here it was unfortunately the opposite. The longer I used given definitions in this style the more awkward they felt, in particular since the rest of the language seemed so much better put together by comparison. And I believe many others agree with me on this. Since the current syntax is unnatural and esoteric, this means it's difficult to discover and very foreign even after that. This makes it much harder to learn and apply givens than it need be. + +### New Given Syntax + +Things become much simpler if we introduce the optional name instead with an `as name` clause at the end, just like we did for context bounds. We can then use a more intuitive syntax for givens like this: +```scala +given Ord[String]: + def compare(x: String, y: String) = ... + +given [A : Ord] => Ord[List[A]]: + def compare(x: List[A], y: List[A]) = ... + +given Monoid[Int]: + extension (x: Int) def combine(y: Int) = x + y + def unit = 0 +``` +If explicit names are desired, we add them with `as` clauses: +```scala +given Ord[String] as stringOrd: + def compare(x: String, y: String) = ... + +given [A : Ord] => Ord[List[A]] as listOrd: + def compare(x: List[A], y: List[A]) = ... + +given Monoid[Int] as intMonoid: + extension (x: Int) def combine(y: Int) = x + y + def unit = 0 +``` + +The underlying principles are: + + - A `given` clause consists of the following elements: + + - An optional _precondition_, which introduces type parameters and/or using clauses and which ends in `=>`, + - the implemented _type_, + - an optional name binding using `as`, + - an implementation which consists of either an `=` and an expression, + or a template body. + + - Since there is no longer a middle `:` separating name and parameters from the implemented type, we can use a `:` to start the class body without looking unnatural, as is done everywhere else. That eliminates the special case where `with` was used before. + +This will be a fairly significant change to the given syntax. I believe there's still a possibility to do this. Not so much code has migrated to new style givens yet, and code that was written can be changed fairly easily. Specifically, there are about a 900K definitions of `implicit def`s +in Scala code on Github and about 10K definitions of `given ... with`. So about 1% of all code uses the Scala 3 syntax, which would have to be changed again. + +Changing something introduced just recently in Scala 3 is not fun, +but I believe these adjustments are preferable to let bad syntax +sit there and fester. The cost of changing should be amortized by improved developer experience over time, and better syntax would also help in migrating Scala 2 style implicits to Scala 3. But we should do it quickly before a lot more code +starts migrating. + +Migration to the new syntax is straightforward, and can be supported by automatic rewrites. For a transition period we can support both the old and the new syntax. It would be a good idea to backport the new given syntax to the LTS version of Scala so that code written in this version can already use it. The current LTS would then support old and new-style givens indefinitely, whereas new Scala 3.x versions would phase out the old syntax over time. + + +### Abolish Abstract Givens + +Another simplification is possible. So far we have special syntax for abstract givens: +```scala +given x: T +``` +The problem is that this syntax clashes with the quite common case where we want to establish a given without any nested definitions. For instance +consider a given that constructs a type tag: +```scala +class Tag[T] +``` +Then this works: +```scala +given Tag[String]() +given Tag[String] with {} +``` +But the following more natural syntax fails: +```scala +given Tag[String] +``` +The last line gives a rather cryptic error: +``` +1 |given Tag[String] + | ^ + | anonymous given cannot be abstract +``` +The problem is that the compiler thinks that the last given is intended to be abstract, and complains since abstract givens need to be named. This is another annoying dissonance. Nowhere else in Scala's syntax does adding a +`()` argument to a class cause a drastic change in meaning. And it's also a violation of the principle that it should be possible to define all givens without providing names for them. + +Fortunately, abstract givens are no longer necessary since they are superseded by the new `deferred` scheme. So we can deprecate that syntax over time. Abstract givens are a highly specialized mechanism with a so far non-obvious syntax. We have seen that this syntax clashes with reasonable expectations of Scala programmers. My estimate is that maybe a dozen people world-wide have used abstract givens in anger so far. + +**Proposal** In the future, let the `= deferred` mechanism be the only way to deliver the functionality of abstract givens. + +This is less of a disruption than it might appear at first: + + - `given T` was illegal before since abstract givens could not be anonymous. + It now means a concrete given of class `T` with no member definitions. + - `given x: T` is legacy syntax for an abstract given. + - `given T as x = deferred` is the analogous new syntax, which is more powerful since + it allows for automatic instantiation. + - `given T = deferred` is the anonymous version in the new syntax, which was not expressible before. + +**Benefits:** + + - Simplification of the language since a feature is dropped + - Eliminate non-obvious and misleading syntax. + +## Summary of Syntax Changes + +Here is the complete context-free syntax for all proposed features. +Overall the syntax for givens becomes a lot simpler than what it was before. + +``` +TmplDef ::= 'given' GivenDef +GivenDef ::= [GivenConditional '=>'] GivenSig +GivenConditional ::= [DefTypeParamClause | UsingParamClause] {UsingParamClause} +GivenSig ::= GivenType ['as' id] ([‘=’ Expr] | TemplateBody) + | ConstrApps ['as' id] TemplateBody +GivenType ::= AnnotType {id [nl] AnnotType} + +TypeDef ::= id [TypeParamClause] TypeAndCtxBounds +TypeParamBounds ::= TypeAndCtxBounds +TypeAndCtxBounds ::= TypeBounds [‘:’ ContextBounds] +ContextBounds ::= ContextBound | '{' ContextBound {',' ContextBound} '}' +ContextBound ::= Type ['as' id] +``` + +## Summary + +The proposed set of changes removes awkward syntax and makes dealing with context bounds and givens a lot more regular and pleasant. In summary, the proposed changes are: + + 1. Allow to name context bounds with `as` clauses. + 2. Give useful default names to other context bounds. + 3. Introduce a less cryptic syntax for multiple context bounds. + 4. Allow context bounds on type members which expand to deferred givens. + 5. Introduce a more regular and clearer syntax for givens. + 6. Eliminate abstract givens. + +These changes were implemented as part of a [draft PR](https://github.com/lampepfl/dotty/pulls/odersky) +which also covers the other prospective changes slated to be proposed in two future SIPs. The new system has proven to work well and to address several fundamental issues people were having with +existing implementation techniques for type classes. + +The changes proposed in this pre-SIP are time-sensitive since we would like to correct some awkward syntax choices in Scala 3 before more code migrates to the new constructs (so far, it seems most code still uses Scala 2 style implicits, which will eventually be phased out). It is easy to migrate to the new syntax and to support both old and new for a transition period. diff --git a/docs/_docs/reference/experimental/typeclasses.md b/docs/_docs/reference/experimental/typeclasses.md index b7dd8cd5fbc5..06c4654098d5 100644 --- a/docs/_docs/reference/experimental/typeclasses.md +++ b/docs/_docs/reference/experimental/typeclasses.md @@ -54,8 +54,6 @@ requires that `Ordering` is a trait or class with a single type parameter (which trait Monoid extends SemiGroup: def unit: Self - object Monoid - def unit(using m: Monoid): m.Self = m.unit trait Functor: type Self[A] @@ -147,68 +145,117 @@ This makes writing instance definitions quite pleasant. Examples: ## Naming Context Bounds -Context bounds are a convenient and legible abbreviation. A problem so far is that they are always anonymous, one cannot name the using parameter to which a context bound expands. For instance, without the trick of defining a universal "trampoline" `unit` in the `Monoid` companion object, we would have to write `reduce` like this: +Context bounds are a convenient and legible abbreviation. A problem so far is that they are always anonymous, +one cannot name the using parameter to which a context bound expands. + +For instance, consider a `reduce` method over `Monoid`s defined like this: + ```scala - def reduce[A](xs: List[A])(using m: A forms Monoid) = - xs.foldLeft(m.unit)(_ `combine` _) +def reduce[A : Monoid](xs: List[A]): A = ??? +``` +Since we don't have a name for the `Monoid` instance of `A`, we need to resort to `summon` in the body of `reduce`: +```scala +def reduce[A : Monoid](xs: List[A]): A = + xs.foldLeft(summon Monoid[A])(_ `combine` _) +``` +That's generally considered too painful to write and read, hence people usually adopt one of two alternatives. Either, eschew context bounds and switch to using clauses: +```scala +def reduce[A](xs: List[A])(using m: Monoid[A]): A = + xs.foldLeft(m)(_ `combine` _) ``` +Or, plan ahead and define a "trampoline" method in `Monoid`'s companion object: +```scala + trait Monoid[A] extends SemiGroup[A]: + def unit: A + object Monoid: + def unit[A](using m: Monoid[A]): A = m.unit + ... + def reduce[A : Monoid](xs: List[A]): A = + xs.foldLeft(Monoid.unit)(_ `combine` _) +``` +This is all accidental complexity which can be avoided by the following proposal. **Proposal:** Allow to name a context bound, like this: ```scala - def reduce[M: Monoid as m](xs: List[M]): A = + def reduce[A : Monoid as m](xs: List[A]): A = xs.foldLeft(m.unit)(_ `combine` _) ``` We use `as x` after the type to bind the instance to `x`. This is analogous to import renaming, which also introduces a new name for something that comes before. -## New Syntax for Aggregate Context Bounds +**Benefits:** The new syntax is simple and clear. +It avoids the awkward choice between concise context bounds that can't be named and verbose using clauses that can. -Aggregate context bounds like `A: X: Y` are not obvious to read, and it becomes worse when we add names, e.g. `A : X as x : Y as y`. +### New Syntax for Aggregate Context Bounds + +Aggregate context bounds like `A : X : Y` are not obvious to read, and it becomes worse when we add names, e.g. `A : X as x : Y as y`. **Proposal:** Allow to combine several context bounds inside `{...}`, analogous -to import clauses. +to import clauses. Example: ```scala - def showMax[X : {Ordered as ordering, Show as show}](x: X, y: X): String = - show.asString(ordering.max(x, y)) + trait: + def showMax[X : {Ordering, Show}](x: X, y: X): String + class B extends A: + def showMax[X : {Ordering as ordering, Show as show}](x: X, y: X): String = + show.asString(ordering.max(x, y)) ``` The old syntax with multiple `:` should be phased out over time. -## Better Default Names for Context Bounds +**Benefits:** The new syntax is much clearer than the old one, in particular for newcomers that don't know context bounds well. + +### Better Default Names for Context Bounds -So far, a context bound for a type `M` gets a synthesized fresh name. It would be much more useful if it got the name of the constrained type instead, translated to be a term name. This means our `reduce` method over monoids would not even need an `as` binding. We could simply formulate it as follows: +So far, an unnamed context bound for a type parameter gets a synthesized fresh name. It would be much more useful if it got the name of the constrained type parameter instead, translated to be a term name. This means our `reduce` method over monoids would not even need an `as` binding. We could simply formulate it as follows: ``` - def reduce[M: Monoid](xs: List[M]) = - xs.foldLeft(M.unit)(_ `combine` _) + def reduce[A : Monoid](xs: List[A]) = + xs.foldLeft(A.unit)(_ `combine` _) ``` +The use of a name like `A` above in two variants, both as a type name and as a term name is of course familiar to Scala programmers. We use the same convention for classes and companion objects. In retrospect, the idea of generalizing this to also cover type parameters is obvious. It is surprising that it was not brought up before. + **Proposed Rules** - 1. The generated evidence parameter for a context bound `M: C as m` has name `m` - 2. The generated evidence for a context bound `M: C` without an `as` binding has name `M` (seen as a term name). So, `M: C` is equivalent to `M: C as M`. + 1. The generated evidence parameter for a context bound `A : C as a` has name `a` + 2. The generated evidence for a context bound `A : C` without an `as` binding has name `A` (seen as a term name). So, `A : C` is equivalent to `A : C as A`. 3. If there are more than one context bounds for a type parameter, the generated evidence parameter for every context bound except the first one has a fresh synthesized name, unless the context bound carries an `as` clause, in which case rule (1) applies. -## Expansion of Context Bounds +The default naming convention reduces the need for named context bounds. But named context bounds are still essential, for at least two reasons: -Context bounds are currently translated to implicit parameters in the last parameter list of a method or class. This is a problem if a context bound is mentioned in one of the preceding parameter types. Example: + - They are needed to give names to multiple context bounds. + - They give an explanation what a single unnamed context bound expands to. + + +### Expansion of Context Bounds + +Context bounds are currently translated to implicit parameters in the last parameter list of a method or class. This is a problem if a context bound is mentioned in one of the preceding parameter types. For example, consider a type class of parsers with associated type members `Input` and `Result` describing the input type on which the parsers operate and the type of results they produce: ```scala - def f[C: ParserCombinator](x: C.Input) = ... +trait Parser[P]: + type Input + type Result ``` -With the current translation, this would give +Here is a method `run` that runs a parser on an input of the required type: + ```scala - def f[C](x: C.input)(using C: C forms ParserCombinator) +def run[P : Parser](in: P.Input): P.Result ``` -But this is ill-typed, since the `C` in `C.input` refers to the `C` introduced in the using clause, which comes later. +Or, making clearer what happens by using an explicit name for the context bound: +```scala +def run[P : Parser as p](in: p.Input): p.Result +``` +With the current translation this does not work since it would be expanded to: +```scala + def run[P](x: p.Input)(using p: Parser[P]): p.Result +``` +Note that the `p` in `p.Input` refers to the `p` introduced in the using clause, which comes later. So this is ill-formed. -This problem would be fixed by changing the translation of context bounds so that they expand to using clauses immediately after the type parameter. But such a change is -infeasible, for two reasons: +This problem would be fixed by changing the translation of context bounds so that they expand to using clauses immediately after the type parameter. But such a change is infeasible, for two reasons: 1. It would be a binary-incompatible change. 2. Putting using clauses earlier can impair type inference. A type in a using clause can be constrained by term arguments coming before that - clause. Moving the using clause first would miss those constraints, which - could cause ambiguities in implicit search. + clause. Moving the using clause first would miss those constraints, which could cause ambiguities in implicit search. But there is an alternative which is feasible: @@ -228,65 +275,88 @@ references to these parameters to be precise, so that information about dependen ## Context Bounds for Type Members -It's not very orthogonal to allow subtype bounds for both type parameters and abstract type members, but context bounds only for type parameters. By moving more of the type class logic to type members this lack becomes more of a problem. +It's not very orthogonal to allow subtype bounds for both type parameters and abstract type members, but context bounds only for type parameters. What's more, we don't even have the fallback of an explicit using clause for type members. The only alternative is to also introduce a set of abstract givens that get implemented in each subclass. This is extremely heavyweight and opaque to newcomers. **Proposal**: Allow context bounds for type members. Example: ```scala - class C: - type Element: Ordered + class Collection: + type Element : Ord ``` The question is how these bounds are expanded. Context bounds on type parameters -are expanded into using clauses. But for type members this does not work, since we cannot refer to a member type of a class in a parameter type of that class. What we are after is an equivalent of using parameter clauses but represented as class members. This is basically a list of deferred givens which can be filled automatically on object creation. +are expanded into using clauses. But for type members this does not work, since we cannot refer to a member type of a class in a parameter type of that class. What we are after is an equivalent of using parameter clauses but represented as class members. -**Proposal:** Introduce a new kind of given definition of the form: +**Proposal:** Introduce a new way to implement a given definition in a trait like this: ```scala given T = deferred ``` -`deferred` is a soft keyword which has special meaning only in this context. -A given with `deferred` right hand side can appear only as a member definition of some trait. Any class implementing that trait will provide an implementation of this given. If a definition is not provided explicitly, it will be synthesized by searching for a given of type `T` in the scope of the inheriting class `C`. Specifically, the scope in which this given will be searched is the environment of `C` augmented by the parameters of `C` but not containing the members of `C` (since that would lead to recursive resolutions). +`deferred` is a new method in the `scala.compiletime` package, which can appear only as the right hand side of a given defined in a trait. Any class implementing that trait will provide an implementation of this given. If a definition is not provided explicitly, it will be synthesized by searching for a given of type `T` in the scope of the inheriting class. Specifically, the scope in which this given will be searched is the environment of that class augmented by its parameters but not containing its members (since that would lead to recursive resolutions). If an implementation _is_ provided explicitly, it counts as an override of a concrete definition and needs an `override` modifier. -A context bound +Deferred givens allow a clean implementation of context bounds in traits, +as in the following example: ```scala -type T: C +trait Sorted: + type Element : Ord + +class SortedSet[A : Ord] extends Sorted: + type Element = A ``` -in a trait will then expand to +The compiler expands this to the following implementation: ```scala -type T -given T forms C = deferred +trait Sorted: + type Element + given Ord[Element] = compiletime.deferred + +class SortedSet[A](using A: Ord[A]) extends Sorted: + type Element = A + override given Ord[Element] = A // i.e. the A defined by the using clause ``` +The using clause in class `SortedSet` provides an implementation for the deferred given in trait `Sorted`. + +**Benefits:** + + - Better orthogonality, type parameters and abstract type members now accept the same kinds of bounds. + - Better ergonomics, since deferred givens get naturally implemented in inheriting classes, no need for boilerplate to fill in definitions of abstract givens. + +**Alternative:** It was suggested that we use a modifier for a deferred given instead of a `= deferred`. Something like `deferred given C[T]`. But a modifier does not suggest the concept that a deferred given will be implemented automatically in subclasses unless an explicit definition is written. In a sense, we can see `= deferred` as the invocation of a magic macro that is provided by the compiler. So from a user's point of view a given with `deferred` right hand side is not abstract. +It is a concrete definition where the compiler will provide the correct implementation. ## New Given Syntax -Our goal is to revise the `given` syntax so that conditional givens don't have to be written anymore like this: +A good language syntax is like a Bach fugue: A small set of motifs is combined in a multitude of harmonic ways. Dissonances and irregularities should be avoided. +When designing Scala 3, I believe that, by and large, we achieved that goal, except in one area, which is the syntax of givens. There _are_ some glaring dissonances, as seen in this code for defining an ordering on lists: ```scala given [A](using Ord[A]): Ord[List[A]] with def compare(x: List[A], y: List[A]) = ... ``` -or, using an explicit name, like this: -```scala -given listOrd[A](using Ord[A]): Ord[List[A]] with - def compare(x: List[A], y: List[A]) = ... -``` -The named version below makes sort of sense, but the unnamed version above is annoyingly irregular compared to other Scala language elements. In particular: +The `:` feels utterly foreign in this position. It's definitely not a type ascription, so what is its role? Just as bad is the trailing `with`. Everywhere else we use braces or trailing `:` to start a scope of nested definitions, so the need of `with` sticks out like a sore thumb. - - There is the `:` that separates parameter clauses and the implemented type class. - This feels out of place, in particular in the unnamed version. - - There is the `with` at the end, which is the only place in Scala where `with` starts a block with definitions. Everywhere else we use `:` or `=`. +We arrived at that syntax not because of a flight of fancy but because even after trying for about a year to find other solutions it seemed like the least bad alternative. The awkwardness of the given syntax arose because we insisted that givens could be named or anonymous, with the default on anonymous, that we would not use underscore for an anonymous given, and that the name, if present, had to come first, and have the form `name [parameters] :`. In retrospect, that last requirement showed a lack of creativity on our part. -The awkwardness of the given syntax was forced upon us since we insisted that givens could be named or anonymous, that we would not use underscore for an anonymous given, and that the name, if present, had to come first. Things become much simpler if we introduce the optional name instead with an `as name` clause at the end. We get uniformity with context bounds, -and we can use a more intuitive syntax for givens like this: +Sometimes unconventional syntax grows on you and becomes natural after a while. But here it was unfortunately the opposite. The longer I used given definitions in this style the more awkward they felt, in particular since the rest of the language seemed so much better put together by comparison. And I believe many others agree with me on this. Since the current syntax is unnatural and esoteric, this means it's difficult to discover and very foreign even after that. This makes it much harder to learn and apply givens than it need be. +Things become much simpler if we introduce the optional name instead with an `as name` clause at the end, just like we did for context bounds. We can then use a more intuitive syntax for givens like this: ```scala -given Int forms Ord: - def compare(x: A, y: A) = ... +given String forms Ord: + def compare(x: String, y: String) = ... + +given [A : Ord] => List[A] forms Ord: + def compare(x: List[A], y: List[A]) = ... -given [A: Ord] => List[A] forms Ord: - def compare(x: A, y: A) = - ... +given Int forms Monoid: + extension (x: Int) def combine(y: Int) = x + y + def unit = 0 +``` +If explicit names are desired, we add them with `as` clauses: +```scala +given String forms Ord as intOrd: + def compare(x: String, y: String) = ... + +given [A : Ord] => List[A] forms Ord as listOrd: + def compare(x: List[A], y: List[A]) = ... given Int forms Monoid as intMonoid: extension (x: Int) def combine(y: Int) = x + y @@ -303,38 +373,70 @@ The underlying principles are: - an implementation which consists of either an `=` and an expression, or a template body. - - We get the pleasing ` forms ` syntax simply by using the predefined infix type `forms`. - - Since there is no more middle `:` separating name and parameters from the implemented type, we can use a `:` to start the class body without looking unnatural. That eliminates the special case where `with` was used before. + - Since there is no longer a middle `:` separating name and parameters from the implemented type, we can use a `:` to start the class body without looking unnatural, as is done everywhere else. That eliminates the special case where `with` was used before. -This will be a fairly significant change to the given syntax. I believe there's still a possibility to do this, -if we are convinced that this is a clear improvement. Not so much code has migrated to new style givens yet, and code that was written can probably be changed fairly easily. But we the longer we wait, the harder it would get. +This will be a fairly significant change to the given syntax. I believe there's still a possibility to do this. Not so much code has migrated to new style givens yet, and code that was written can be changed fairly easily. Specifically, there are about a 900K definitions of `implicit def`s +in Scala code on Github and about 10K definitions of `given ... with`. So about 1% of all code uses the Scala 3 syntax, which would have to be changed again. -Migration to the new syntax should be straightforward. For a transition period we can support both the old and the new syntax. And we can offer rewrite rules that turn old into new. +Changing something introduced just recently in Scala 3 is not fun, +but I believe these adjustments are preferable to let bad syntax +sit there and fester. The cost of changing should be amortized by improved developer experience over time, and better syntax would also help in migrating Scala 2 style implicits to Scala 3. But we should do it quickly before a lot more code +starts migrating. -## Abolish Abstract Givens +Migration to the new syntax is straightforward, and can be supported by automatic rewrites. For a transition period we can support both the old and the new syntax. It would be a good idea to backport the new given syntax to the LTS version of Scala so that code written in this version can already use it. The current LTS would then support old and new-style givens indefinitely, whereas new Scala 3.x versions would phase out the old syntax over time. -So far we have special syntax for abstract givens: + +### Abolish Abstract Givens + +Another simplification is possible. So far we have special syntax for abstract givens: ```scala given x: T ``` -This should be no longer necessary with the `deferred` scheme, so I propose to deprecate that syntax (over time). Abstract givens are a highly specialized mechanism with a (so far) non-obvious syntax. My estimate is that maybe a dozen people world-wide have used them in anger so far. +The problem is that this syntax clashes with the quite common case where we want to establish a given without any nested definitions. For instance +consider a given that constructs a type tag: +```scala +class Tag[T] +``` +Then this works: +```scala +given Tag[String]() +given Tag[String] with {} +``` +But the following more natural syntax fails: +```scala +given Tag[String] +``` +The last line gives a rather cryptic error: +``` +1 |given Tag[String] + | ^ + | anonymous given cannot be abstract +``` +The problem is that the compiler thinks that the last given is intended to be abstract, and complains since abstract givens need to be named. This is another annoying dissonance. Nowhere else in Scala's syntax does adding a +`()` argument to a class cause a drastic change in meaning. And it's also a violation of the principle that it should be possible to define all givens without providing names for them. -**Proposal** In the future, let the `= deferred` mechanism be the only way to define an abstract given. +Fortunately, abstract givens are no longer necessary since they are superseded by the new `deferred` scheme. So we can deprecate that syntax over time. Abstract givens are a highly specialized mechanism with a so far non-obvious syntax. We have seen that this syntax clashes with reasonable expectations of Scala programmers. My estimate is that maybe a dozen people world-wide have used abstract givens in anger so far. + +**Proposal** In the future, let the `= deferred` mechanism be the only way to deliver the functionality of abstract givens. This is less of a disruption than it might appear at first: - `given T` was illegal before since abstract givens could not be anonymous. - It now means a concrete given of class `T` with no member definitions. This - is the natural interpretation for simple tagging given clauses such as - `given String forms Value`. - - `given x: T` is legacy syntax for a deferred given. + It now means a concrete given of class `T` with no member definitions. + - `given x: T` is legacy syntax for an abstract given. - `given T as x = deferred` is the analogous new syntax, which is more powerful since it allows for automatic instantiation. - `given T = deferred` is the anonymous version in the new syntax, which was not expressible before. -## Syntax Changes +**Benefits:** + + - Simplification of the language since a feature is dropped + - Eliminate non-obvious and misleading syntax. + +## Summary of Syntax Changes -The changed syntax is here. Overall the syntax for givens becomes a lot simpler than what it was before. +Here is the complete context-free syntax for all proposed features. +Overall the syntax for givens becomes a lot simpler than what it was before. ``` TmplDef ::= 'given' GivenDef @@ -344,7 +446,7 @@ GivenSig ::= GivenType ['as' id] ([‘=’ Expr] | TemplateBody) | ConstrApps ['as' id] TemplateBody GivenType ::= AnnotType {id [nl] AnnotType} -TypeDef ::= id [TypeParamClause] {FunParamClause} TypeAndCtxBounds +TypeDef ::= id [TypeParamClause] TypeAndCtxBounds TypeParamBounds ::= TypeAndCtxBounds TypeAndCtxBounds ::= TypeBounds [‘:’ ContextBounds] ContextBounds ::= ContextBound | '{' ContextBound {',' ContextBound} '}' From 683507a917a564cf7b977aa40891159b7af720d1 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 27 Feb 2024 19:40:23 +0100 Subject: [PATCH 54/63] Add compiletime.deferred to MimaFilters --- project/MiMaFilters.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 529c238c38b7..aad09834dcd1 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -10,6 +10,7 @@ object MiMaFilters { Build.previousDottyVersion -> Seq( ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.modularity"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$modularity$"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.compiletime.package#package.deferred"), ), // Additions since last LTS From 84b8a21a85eef6954ab9d7066ac73037dbc5429b Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 28 Feb 2024 14:18:29 +0100 Subject: [PATCH 55/63] Changes to default names for context bound witnesses 1. Use the constrained type's name as a term name only for single context bounds 2. Apply the same scheme to deferred givens --- .../src/dotty/tools/dotc/ast/Desugar.scala | 13 +-- .../experimental/typeclasses-syntax.md | 15 ++- .../reference/experimental/typeclasses.md | 13 ++- tests/neg/FromString.scala | 12 +++ tests/pos/FromString.scala | 4 +- tests/pos/deferred-givens.scala | 13 +++ tests/run/for-desugar-strawman.scala | 96 +++++++++++++++++++ 7 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 tests/neg/FromString.scala create mode 100644 tests/run/for-desugar-strawman.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index af1b017d718d..79b5889aedf4 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -232,13 +232,14 @@ object desugar { flags: FlagSet, freshName: => TermName)(using Context): Tree = rhs match case ContextBounds(tbounds, cxbounds) => - var useParamName = Feature.enabled(Feature.modularity) for bound <- cxbounds do val evidenceName = bound match - case ContextBoundTypeTree(_, _, ownName) if !ownName.isEmpty => ownName - case _ if useParamName => tname.toTermName - case _ => freshName - useParamName = false + case ContextBoundTypeTree(_, _, ownName) if !ownName.isEmpty => + ownName + case _ if Feature.enabled(Feature.modularity) && cxbounds.tail.isEmpty => + tname.toTermName + case _ => + freshName val evidenceParam = ValDef(evidenceName, bound, EmptyTree).withFlags(flags) evidenceParam.pushAttachment(ContextBoundParam, ()) evidenceBuf += evidenceParam @@ -484,7 +485,7 @@ object desugar { val evidenceBuf = new ListBuffer[ValDef] val result = cpy.TypeDef(tdef)(rhs = desugarContextBounds(tdef.name, tdef.rhs, evidenceBuf, - (tdef.mods.flags.toTermFlags & AccessFlags) | DeferredGivenFlags, EmptyTermName)) + (tdef.mods.flags.toTermFlags & AccessFlags) | Lazy | DeferredGivenFlags, EmptyTermName)) if evidenceBuf.isEmpty then result else Thicket(result :: evidenceBuf.toList) /** The expansion of a class definition. See inline comments for what is involved */ diff --git a/docs/_docs/reference/experimental/typeclasses-syntax.md b/docs/_docs/reference/experimental/typeclasses-syntax.md index bdafe9d6381c..03e64be1955f 100644 --- a/docs/_docs/reference/experimental/typeclasses-syntax.md +++ b/docs/_docs/reference/experimental/typeclasses-syntax.md @@ -90,13 +90,15 @@ So far, an unnamed context bound for a type parameter gets a synthesized fresh n xs.foldLeft(A.unit)(_ `combine` _) ``` + + The use of a name like `A` above in two variants, both as a type name and as a term name is of course familiar to Scala programmers. We use the same convention for classes and companion objects. In retrospect, the idea of generalizing this to also cover type parameters is obvious. It is surprising that it was not brought up before. **Proposed Rules** 1. The generated evidence parameter for a context bound `A : C as a` has name `a` 2. The generated evidence for a context bound `A : C` without an `as` binding has name `A` (seen as a term name). So, `A : C` is equivalent to `A : C as A`. - 3. If there are more than one context bounds for a type parameter, the generated evidence parameter for every context bound except the first one has a fresh synthesized name, unless the context bound carries an `as` clause, in which case rule (1) applies. + 3. If there are multiple context bounds for a type parameter, as in `A : {C_1, ..., C_n}`, the generated evidence parameter for every context bound `C_i` has a fresh synthesized name, unless the context bound carries an `as` clause, in which case rule (1) applies. The default naming convention reduces the need for named context bounds. But named context bounds are still essential, for at least two reasons: @@ -183,15 +185,22 @@ The compiler expands this to the following implementation: ```scala trait Sorted: type Element - given Ord[Element] = compiletime.deferred + given Ord[Element] as Element = compiletime.deferred class SortedSet[A](using A: Ord[A]) extends Sorted: type Element = A - override given Ord[Element] = A // i.e. the A defined by the using clause + override given Ord[Element] as Element = A // i.e. the A defined by the using clause ``` The using clause in class `SortedSet` provides an implementation for the deferred given in trait `Sorted`. +If there is a single context bound, as in +```scala + type T : C +``` +the synthesized deferred given will get the (term-)name of the constrained type `T`. If there are multiple bounds, +the standard convention for naming anonymous givens applies. + **Benefits:** - Better orthogonality, type parameters and abstract type members now accept the same kinds of bounds. diff --git a/docs/_docs/reference/experimental/typeclasses.md b/docs/_docs/reference/experimental/typeclasses.md index 06c4654098d5..dd74cf5579ec 100644 --- a/docs/_docs/reference/experimental/typeclasses.md +++ b/docs/_docs/reference/experimental/typeclasses.md @@ -219,7 +219,7 @@ The use of a name like `A` above in two variants, both as a type name and as a t 1. The generated evidence parameter for a context bound `A : C as a` has name `a` 2. The generated evidence for a context bound `A : C` without an `as` binding has name `A` (seen as a term name). So, `A : C` is equivalent to `A : C as A`. - 3. If there are more than one context bounds for a type parameter, the generated evidence parameter for every context bound except the first one has a fresh synthesized name, unless the context bound carries an `as` clause, in which case rule (1) applies. + 3. If there are multiple context bounds for a type parameter, as in `A : {C_1, ..., C_n}`, the generated evidence parameter for every context bound `C_i` has a fresh synthesized name, unless the context bound carries an `as` clause, in which case rule (1) applies. The default naming convention reduces the need for named context bounds. But named context bounds are still essential, for at least two reasons: @@ -306,15 +306,22 @@ The compiler expands this to the following implementation: ```scala trait Sorted: type Element - given Ord[Element] = compiletime.deferred + given Ord[Element] as Element = compiletime.deferred class SortedSet[A](using A: Ord[A]) extends Sorted: type Element = A - override given Ord[Element] = A // i.e. the A defined by the using clause + override given Ord[Element] as Element = A // i.e. the A defined by the using clause ``` The using clause in class `SortedSet` provides an implementation for the deferred given in trait `Sorted`. +If there is a single context bound, as in +```scala + type T : C +``` +the synthesized deferred given will get the (term-)name of the constrained type `T`. If there are multiple bounds, +the standard convention for naming anonymous givens applies. + **Benefits:** - Better orthogonality, type parameters and abstract type members now accept the same kinds of bounds. diff --git a/tests/neg/FromString.scala b/tests/neg/FromString.scala new file mode 100644 index 000000000000..b81c72aec530 --- /dev/null +++ b/tests/neg/FromString.scala @@ -0,0 +1,12 @@ +//> using options -language:experimental.modularity -source future + +trait FromString: + type Self + def fromString(s: String): Self + +given Int forms FromString = _.toInt + +given Double forms FromString = _.toDouble + +def add[N: {FromString, Numeric as num}](a: String, b: String): N = + num.plus(N.fromString(a), N.fromString(b)) // error: Not found: N // error diff --git a/tests/pos/FromString.scala b/tests/pos/FromString.scala index 488e4b7f7404..4da8f1c9833a 100644 --- a/tests/pos/FromString.scala +++ b/tests/pos/FromString.scala @@ -8,5 +8,5 @@ given Int forms FromString = _.toInt given Double forms FromString = _.toDouble -def add[N: {FromString, Numeric as num}](a: String, b: String): N = - num.plus(N.fromString(a), N.fromString(b)) +def add[N: {FromString as fs, Numeric as num}](a: String, b: String): N = + num.plus(fs.fromString(a), fs.fromString(b)) diff --git a/tests/pos/deferred-givens.scala b/tests/pos/deferred-givens.scala index 049a6a0860d1..3419ac8d8a4d 100644 --- a/tests/pos/deferred-givens.scala +++ b/tests/pos/deferred-givens.scala @@ -1,6 +1,18 @@ //> using options -language:experimental.modularity -source future import compiletime.* class Ord[Elem] +given Ord[Double] + +trait A: + type Elem : Ord + def foo = summon[Ord[Elem]] + +class AC extends A: + type Elem = Double + override given Ord[Elem] as Elem = ??? + +class AD extends A: + type Elem = Double trait B: type Elem @@ -22,3 +34,4 @@ class E(using x: Ord[String]) extends B: class F[X: Ord] extends B: type Elem = X + diff --git a/tests/run/for-desugar-strawman.scala b/tests/run/for-desugar-strawman.scala new file mode 100644 index 000000000000..a92b19b9150a --- /dev/null +++ b/tests/run/for-desugar-strawman.scala @@ -0,0 +1,96 @@ + +@main def Test = + println: + for + x <- List(1, 2, 3) + y = x + x + if x >= 2 + i <- List.range(0, y) + z = i * i + if z % 2 == 0 + yield + i * x + + println: + val xs = List(1, 2, 3) + xs.flatMapDefined: x => + val y = x + x + xs.applyFilter(x >= 2): + val is = List.range(0, y) + is.mapDefined: i => + val z = i * i + is.applyFilter(z % 2 == 0): + i * x + +extension [A](as: List[A]) + + def applyFilter[B](p: => Boolean)(b: => B) = + if p then Some(b) else None + + def flatMapDefined[B](f: A => Option[IterableOnce[B]]): List[B] = + as.flatMap: x => + f(x).getOrElse(Nil) + + def mapDefined[B](f: A => Option[B]): List[B] = + as.flatMap(f) + +object UNDEFINED + +extension [A](as: Vector[A]) + + def applyFilter[B](p: => Boolean)(b: => B) = + if p then b else UNDEFINED + + def flatMapDefined[B](f: A => IterableOnce[B] | UNDEFINED.type): Vector[B] = + as.flatMap: x => + f(x) match + case UNDEFINED => Nil + case y: IterableOnce[B] => y + + def mapDefined[B](f: A => B | UNDEFINED.type): Vector[B] = + as.flatMap: x => + f(x) match + case UNDEFINED => Nil + case y: B => y :: Nil + +/* +F ::= val x = E; F + x <- E; G +G ::= [] + val x = E; G + if E; G + x <- E; G + +Translation scheme: + +{ for F yield E }c where c = undefined +{ for G yield E }c where c is a reference to the generator preceding the G sequence + +{ for [] yield E }c = E +{ for p = Ep; G yield E }c = val p = Ep; { for G yield E }c +{ for if Ep; G yield E}c = c.applyFilter(Ep)({ for G yield E }c) +{ for p <- Ep; G yield E }c = val c1 = Ep; c1.BIND{ case p => { for G yield E }c1 } (c1 fresh) + + where BIND = flatMapDefined if isGen(G), isFilter(G) + = mapDefined if !isGen(G), isFilter(G) + = flatMap if isGen(G), !isFilter(G) + = map if !isGen(G), !isFilter(G) + +{ for case p <- Ep; G yield E }c = { for $x <- Ep; if $x match case p => true case _ => false; p = $x@RuntimeChecked; G yield E }c +{ for case p = Ep; G yield E }c = { for $x = Ep; if $x match case p => true case _ => false; p = $x@RuntimeChecked; G yield E}c + +isFilter(if E; S) +isFilter(val x = E; S) if isFilter(S) + +isGen(x <- E; S) +isGen(val x = E; S) if isGen(S) +isGen(if E; S) if isGen(S) + +*/ + +val foo = 1 + +def main2 = + foo + ??? + ??? match { case _ => 0 } \ No newline at end of file From 8c2f6dffeed91264326a7972e4b50a75a5c61913 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 29 Feb 2024 10:42:21 +0100 Subject: [PATCH 56/63] Fix typo --- docs/_docs/reference/experimental/typeclasses.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/reference/experimental/typeclasses.md b/docs/_docs/reference/experimental/typeclasses.md index dd74cf5579ec..91541400ed13 100644 --- a/docs/_docs/reference/experimental/typeclasses.md +++ b/docs/_docs/reference/experimental/typeclasses.md @@ -126,7 +126,7 @@ and not to: We introduce a standard type alias `forms` in the Scala package or in `Predef`, defined like this: ```scala - infix type is[A <: AnyKind, B <: {type Self <: AnyKind}] = B { type Self = A } + infix type forms[A <: AnyKind, B <: {type Self <: AnyKind}] = B { type Self = A } ``` This makes writing instance definitions quite pleasant. Examples: From ff17c716133afcb103af9af9a080879ea09405a2 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 29 Feb 2024 17:37:24 +0100 Subject: [PATCH 57/63] Don't use member type name for deferred givens When expanding a context bound of a member type, don't use the member name as the name of the generated deferred given. The same member type might have different single context bounds in different traits. A class inheriting several of these traits would then get double definitions in its given clauses. Partial revert of "Changes to default names for context bound witnesses" --- compiler/src/dotty/tools/dotc/ast/Desugar.scala | 8 ++++++-- .../reference/experimental/typeclasses-syntax.md | 11 ++--------- docs/_docs/reference/experimental/typeclasses.md | 11 ++--------- tests/pos/deferred-givens.scala | 3 +-- 4 files changed, 11 insertions(+), 22 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 79b5889aedf4..674d66d29b35 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -232,14 +232,16 @@ object desugar { flags: FlagSet, freshName: => TermName)(using Context): Tree = rhs match case ContextBounds(tbounds, cxbounds) => + val isMember = flags.isAllOf(DeferredGivenFlags) for bound <- cxbounds do val evidenceName = bound match case ContextBoundTypeTree(_, _, ownName) if !ownName.isEmpty => ownName - case _ if Feature.enabled(Feature.modularity) && cxbounds.tail.isEmpty => + case _ if !isMember && cxbounds.tail.isEmpty && Feature.enabled(Feature.modularity) => tname.toTermName case _ => - freshName + if isMember then inventGivenOrExtensionName(bound) + else freshName val evidenceParam = ValDef(evidenceName, bound, EmptyTree).withFlags(flags) evidenceParam.pushAttachment(ContextBoundParam, ()) evidenceBuf += evidenceParam @@ -1202,6 +1204,8 @@ object desugar { case tree: TypeDef => tree.name.toString case tree: AppliedTypeTree if followArgs && tree.args.nonEmpty => s"${apply(x, tree.tpt)}_${extractArgs(tree.args)}" + case ContextBoundTypeTree(tycon, paramName, _) => + s"${apply(x, tycon)}_$paramName" case InfixOp(left, op, right) => if followArgs then s"${op.name}_${extractArgs(List(left, right))}" else op.name.toString diff --git a/docs/_docs/reference/experimental/typeclasses-syntax.md b/docs/_docs/reference/experimental/typeclasses-syntax.md index 03e64be1955f..1d746a995dad 100644 --- a/docs/_docs/reference/experimental/typeclasses-syntax.md +++ b/docs/_docs/reference/experimental/typeclasses-syntax.md @@ -185,22 +185,15 @@ The compiler expands this to the following implementation: ```scala trait Sorted: type Element - given Ord[Element] as Element = compiletime.deferred + given Ord[Element] = compiletime.deferred class SortedSet[A](using A: Ord[A]) extends Sorted: type Element = A - override given Ord[Element] as Element = A // i.e. the A defined by the using clause + override given Ord[Element] = A // i.e. the A defined by the using clause ``` The using clause in class `SortedSet` provides an implementation for the deferred given in trait `Sorted`. -If there is a single context bound, as in -```scala - type T : C -``` -the synthesized deferred given will get the (term-)name of the constrained type `T`. If there are multiple bounds, -the standard convention for naming anonymous givens applies. - **Benefits:** - Better orthogonality, type parameters and abstract type members now accept the same kinds of bounds. diff --git a/docs/_docs/reference/experimental/typeclasses.md b/docs/_docs/reference/experimental/typeclasses.md index 91541400ed13..79abed7be6a8 100644 --- a/docs/_docs/reference/experimental/typeclasses.md +++ b/docs/_docs/reference/experimental/typeclasses.md @@ -306,22 +306,15 @@ The compiler expands this to the following implementation: ```scala trait Sorted: type Element - given Ord[Element] as Element = compiletime.deferred + given Ord[Element] = compiletime.deferred class SortedSet[A](using A: Ord[A]) extends Sorted: type Element = A - override given Ord[Element] as Element = A // i.e. the A defined by the using clause + override given Ord[Element] = A // i.e. the A defined by the using clause ``` The using clause in class `SortedSet` provides an implementation for the deferred given in trait `Sorted`. -If there is a single context bound, as in -```scala - type T : C -``` -the synthesized deferred given will get the (term-)name of the constrained type `T`. If there are multiple bounds, -the standard convention for naming anonymous givens applies. - **Benefits:** - Better orthogonality, type parameters and abstract type members now accept the same kinds of bounds. diff --git a/tests/pos/deferred-givens.scala b/tests/pos/deferred-givens.scala index 3419ac8d8a4d..b9018c97e151 100644 --- a/tests/pos/deferred-givens.scala +++ b/tests/pos/deferred-givens.scala @@ -9,7 +9,7 @@ trait A: class AC extends A: type Elem = Double - override given Ord[Elem] as Elem = ??? + override given Ord[Elem] = ??? class AD extends A: type Elem = Double @@ -34,4 +34,3 @@ class E(using x: Ord[String]) extends B: class F[X: Ord] extends B: type Elem = X - From 733af36f33ad94f47036ad380dd0982c94d39da0 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 21 Dec 2023 11:32:24 +0100 Subject: [PATCH 58/63] Change rules for given prioritization Consider the following program: ```scala class A class B extends A class C extends A given A = A() given B = B() given C = C() def f(using a: A, b: B, c: C) = println(a.getClass) println(b.getClass) println(c.getClass) @main def Test = f ``` With the current rules, this would fail with an ambiguity error between B and C when trying to synthesize the A parameter. This is a problem without an easy remedy. We can fix this problem by flipping the priority for implicit arguments. Instead of requiring an argument to be most _specific_, we now require it to be most _general_ while still conforming to the formal parameter. There are three justifications for this change, which at first glance seems quite drastic: - It gives us a natural way to deal with inheritance triangles like the one in the code above. Such triangles are quite common. - Intuitively, we want to get the closest possible match between required formal parameter type and synthetisized argument. The "most general" rule provides that. - We already do a crucial part of this. Namely, with current rules we interpolate all type variables in an implicit argument downwards, no matter what their variance is. This makes no sense in theory, but solves hairy problems with contravariant typeclasses like `Comparable`. Instead of this hack, we now do something more principled, by flipping the direction everywhere, preferring general over specific, instead of just flipping contravariant type parameters. The behavior is dependent on the Scala version - Old behavior: up to 3.4 - New behavior: from 3.5, 3.5-migration warns on behavior change The CB builds under the new rules. One fix was needed for a shapeless 3 deriving test. There was a typo: mkInstances instead of mkProductInstances, which previously got healed by accident because of the most specific rule. Also: Don't flip contravariant type arguments for overloading resolution Flipping contravariant type arguments was needed for implicit search where it will be replaced by a more general scheme. But it makes no sense for overloading resolution. For overloading resolution, we want to pick the most specific alternative, analogous to us picking the most specific instantiation when we force a fully defined type. Also: Disable implicit search everywhere for disambiaguation Previously, one disambiguation step missed that, whereas implicits were turned off everywhere else. --- .../community-projects/shapeless-3 | 2 +- compiler/src/dotty/tools/dotc/core/Mode.scala | 17 ++- .../dotty/tools/dotc/typer/Applications.scala | 131 +++++++++++------- .../dotty/tools/dotc/typer/Implicits.scala | 29 +++- .../tools/dotc/typer/ImportSuggestions.scala | 2 +- .../changed-features/implicit-resolution.md | 17 ++- tests/neg/i15264.scala | 59 ++++++++ tests/pos/i15264.scala | 5 +- tests/pos/overload-disambiguation.scala | 13 ++ tests/run/given-triangle.check | 3 + tests/run/given-triangle.scala | 16 +++ tests/run/implicit-specifity.scala | 4 +- tests/run/implied-for.scala | 2 +- tests/run/implied-priority.scala | 11 +- tests/warn/given-triangle.check | 6 + tests/warn/given-triangle.scala | 16 +++ 16 files changed, 255 insertions(+), 78 deletions(-) create mode 100644 tests/neg/i15264.scala create mode 100644 tests/pos/overload-disambiguation.scala create mode 100644 tests/run/given-triangle.check create mode 100644 tests/run/given-triangle.scala create mode 100644 tests/warn/given-triangle.check create mode 100644 tests/warn/given-triangle.scala diff --git a/community-build/community-projects/shapeless-3 b/community-build/community-projects/shapeless-3 index d27c5ba1ae51..90f0c977b536 160000 --- a/community-build/community-projects/shapeless-3 +++ b/community-build/community-projects/shapeless-3 @@ -1 +1 @@ -Subproject commit d27c5ba1ae5111b85df2cfb65a26b9246c52570c +Subproject commit 90f0c977b536c06305496600b8b2014c9e8e3d86 diff --git a/compiler/src/dotty/tools/dotc/core/Mode.scala b/compiler/src/dotty/tools/dotc/core/Mode.scala index 71b49394ae14..17a36e4fee32 100644 --- a/compiler/src/dotty/tools/dotc/core/Mode.scala +++ b/compiler/src/dotty/tools/dotc/core/Mode.scala @@ -41,6 +41,8 @@ object Mode { val Pattern: Mode = newMode(0, "Pattern") val Type: Mode = newMode(1, "Type") + val PatternOrTypeBits: Mode = Pattern | Type + val ImplicitsEnabled: Mode = newMode(2, "ImplicitsEnabled") val InferringReturnType: Mode = newMode(3, "InferringReturnType") @@ -101,16 +103,19 @@ object Mode { */ val CheckBoundsOrSelfType: Mode = newMode(14, "CheckBoundsOrSelfType") - /** Use Scala2 scheme for overloading and implicit resolution */ - val OldOverloadingResolution: Mode = newMode(15, "OldOverloadingResolution") + /** Use previous Scheme for implicit resolution. Currently significant + * in 3.0-migration where we use Scala-2's scheme instead and in 3.5-migration + * where we use the previous scheme up to 3.4 instead. + */ + val OldImplicitResolution: Mode = newMode(15, "OldImplicitResolution") /** Treat CapturingTypes as plain AnnotatedTypes even in phase CheckCaptures. - * Reuses the value of OldOverloadingResolution to save Mode bits. - * This is OK since OldOverloadingResolution only affects implicit search, which + * Reuses the value of OldImplicitResolution to save Mode bits. + * This is OK since OldImplicitResolution only affects implicit search, which * is done during phases Typer and Inlinig, and IgnoreCaptures only has an * effect during phase CheckCaptures. */ - val IgnoreCaptures = OldOverloadingResolution + val IgnoreCaptures = OldImplicitResolution /** Allow hk applications of type lambdas to wildcard arguments; * used for checking that such applications do not normally arise @@ -120,8 +125,6 @@ object Mode { /** Read original positions when unpickling from TASTY */ val ReadPositions: Mode = newMode(17, "ReadPositions") - val PatternOrTypeBits: Mode = Pattern | Type - /** We are elaborating the fully qualified name of a package clause. * In this case, identifiers should never be imported. */ diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 82f4c89ae203..fb750f6aec63 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -22,7 +22,7 @@ import ProtoTypes.* import Inferencing.* import reporting.* import Nullables.*, NullOpsDecorator.* -import config.Feature +import config.{Feature, SourceVersion} import collection.mutable import config.Printers.{overload, typr, unapp} @@ -1657,6 +1657,12 @@ trait Applications extends Compatibility { /** Compare two alternatives of an overloaded call or an implicit search. * * @param alt1, alt2 Non-overloaded references indicating the two choices + * @param preferGeneral When comparing two value types, prefer the more general one + * over the more specific one iff `preferGeneral` is true. + * `preferGeneral` is set to `true` when we compare two given values, since + * then we want the most general evidence that matches the target + * type. It is set to `false` for overloading resolution, when we want the + * most specific type instead. * @return 1 if 1st alternative is preferred over 2nd * -1 if 2nd alternative is preferred over 1st * 0 if neither alternative is preferred over the other @@ -1672,27 +1678,28 @@ trait Applications extends Compatibility { * an alternative that takes more implicit parameters wins over one * that takes fewer. */ - def compare(alt1: TermRef, alt2: TermRef)(using Context): Int = trace(i"compare($alt1, $alt2)", overload) { + def compare(alt1: TermRef, alt2: TermRef, preferGeneral: Boolean = false)(using Context): Int = trace(i"compare($alt1, $alt2)", overload) { record("resolveOverloaded.compare") - /** Is alternative `alt1` with type `tp1` as specific as alternative + val compareGivens = alt1.symbol.is(Given) || alt2.symbol.is(Given) + + /** Is alternative `alt1` with type `tp1` as good as alternative * `alt2` with type `tp2` ? * - * 1. A method `alt1` of type `(p1: T1, ..., pn: Tn)U` is as specific as `alt2` + * 1. A method `alt1` of type `(p1: T1, ..., pn: Tn)U` is as good as `alt2` * if `alt1` is nullary or `alt2` is applicable to arguments (p1, ..., pn) of * types T1,...,Tn. If the last parameter `pn` has a vararg type T*, then * `alt1` must be applicable to arbitrary numbers of `T` parameters (which * implies that it must be a varargs method as well). * 2. A polymorphic member of type [a1 >: L1 <: U1, ..., an >: Ln <: Un]T is as - * specific as `alt2` of type `tp2` if T is as specific as `tp2` under the + * good as `alt2` of type `tp2` if T is as good as `tp2` under the * assumption that for i = 1,...,n each ai is an abstract type name bounded * from below by Li and from above by Ui. * 3. A member of any other type `tp1` is: - * a. always as specific as a method or a polymorphic method. - * b. as specific as a member of any other type `tp2` if `tp1` is compatible - * with `tp2`. + * a. always as good as a method or a polymorphic method. + * b. as good as a member of any other type `tp2` is `asGoodValueType(tp1, tp2) = true` */ - def isAsSpecific(alt1: TermRef, tp1: Type, alt2: TermRef, tp2: Type): Boolean = trace(i"isAsSpecific $tp1 $tp2", overload) { + def isAsGood(alt1: TermRef, tp1: Type, alt2: TermRef, tp2: Type): Boolean = trace(i"isAsSpecific $tp1 $tp2", overload) { tp1 match case tp1: MethodType => // (1) tp1.paramInfos.isEmpty && tp2.isInstanceOf[LambdaType] @@ -1714,69 +1721,89 @@ trait Applications extends Compatibility { fullyDefinedType(tp1Params, "type parameters of alternative", alt1.symbol.srcPos) val tparams = newTypeParams(alt1.symbol, tp1.paramNames, EmptyFlags, tp1.instantiateParamInfos(_)) - isAsSpecific(alt1, tp1.instantiate(tparams.map(_.typeRef)), alt2, tp2) + isAsGood(alt1, tp1.instantiate(tparams.map(_.typeRef)), alt2, tp2) } case _ => // (3) tp2 match case tp2: MethodType => true // (3a) case tp2: PolyType if tp2.resultType.isInstanceOf[MethodType] => true // (3a) case tp2: PolyType => // (3b) - explore(isAsSpecificValueType(tp1, instantiateWithTypeVars(tp2))) + explore(isAsGoodValueType(tp1, instantiateWithTypeVars(tp2))) case _ => // 3b) - isAsSpecificValueType(tp1, tp2) + isAsGoodValueType(tp1, tp2) } - /** Test whether value type `tp1` is as specific as value type `tp2`. - * Let's abbreviate this to `tp1 <:s tp2`. - * Previously, `<:s` was the same as `<:`. This behavior is still - * available under mode `Mode.OldOverloadingResolution`. The new behavior - * is different, however. Here, `T <:s U` iff + /** Test whether value type `tp1` is as good as value type `tp2`. + * Let's abbreviate this to `tp1 <:p tp2`. The behavior depends on the Scala version + * and mode. * - * flip(T) <: flip(U) + * - In Scala 2, `<:p` was the same as `<:`. This behavior is still + * available in 3.0-migration if mode `Mode.OldImplicitResolution` is turned on as well. + * It is used to highlight differences between Scala 2 and 3 behavior. * - * where `flip` changes covariant occurrences of contravariant type parameters to - * covariant ones. Intuitively `<:s` means subtyping `<:`, except that all arguments - * to contravariant parameters are compared as if they were covariant. E.g. given class + * - In Scala 3.0-3.4, the behavior is as follows: `T <:p U` iff there is an impliit conversion + * from `T` to `U`, or * - * class Cmp[-X] + * flip(T) <: flip(U) * - * `Cmp[T] <:s Cmp[U]` if `T <: U`. On the other hand, non-variant occurrences - * of parameters are not affected. So `T <: U` would imply `Set[Cmp[U]] <:s Set[Cmp[T]]`, - * as usual, because `Set` is non-variant. + * where `flip` changes covariant occurrences of contravariant type parameters to + * covariant ones. Intuitively `<:p` means subtyping `<:`, except that all arguments + * to contravariant parameters are compared as if they were covariant. E.g. given class * - * This relation might seem strange, but it models closely what happens for methods. - * Indeed, if we integrate the existing rules for methods into `<:s` we have now that + * class Cmp[-X] * - * (T)R <:s (U)R + * `Cmp[T] <:p Cmp[U]` if `T <: U`. On the other hand, non-variant occurrences + * of parameters are not affected. So `T <: U` would imply `Set[Cmp[U]] <:p Set[Cmp[T]]`, + * as usual, because `Set` is non-variant. * - * iff + * - From Scala 3.5, `T <:p U` means `T <: U` or `T` convertible to `U` + * for overloading resolution (when `preferGeneral is false), and the opposite relation + * `U <: T` or `U convertible to `T` for implicit disambiguation between givens + * (when `preferGeneral` is true). For old-style implicit values, the 3.4 behavior is kept. * - * T => R <:s U => R + * - In Scala 3.5-migration, use the 3.5 scheme normally, and the 3.4 scheme if + * `Mode.OldImplicitResolution` is on. This is used to highlight differences in the + * two resolution schemes. * - * Also: If a compared type refers to a given or its module class, use + * Also and only for given resolution: If a compared type refers to a given or its module class, use * the intersection of its parent classes instead. */ - def isAsSpecificValueType(tp1: Type, tp2: Type)(using Context) = - if (ctx.mode.is(Mode.OldOverloadingResolution)) + def isAsGoodValueType(tp1: Type, tp2: Type)(using Context) = + val oldResolution = ctx.mode.is(Mode.OldImplicitResolution) + if !preferGeneral || Feature.migrateTo3 && oldResolution then + // Normal specificity test for overloading resolution (where `preferGeneral` is false) + // and in mode Scala3-migration when we compare with the old Scala 2 rules. isCompatible(tp1, tp2) - else { - val flip = new TypeMap { - def apply(t: Type) = t match { - case t @ AppliedType(tycon, args) => - def mapArg(arg: Type, tparam: TypeParamInfo) = - if (variance > 0 && tparam.paramVarianceSign < 0) defn.FunctionNOf(arg :: Nil, defn.UnitType) - else arg - mapOver(t.derivedAppliedType(tycon, args.zipWithConserve(tycon.typeParams)(mapArg))) - case _ => mapOver(t) - } - } - def prepare(tp: Type) = tp.stripTypeVar match { + else + def prepare(tp: Type) = tp.stripTypeVar match case tp: NamedType if tp.symbol.is(Module) && tp.symbol.sourceModule.is(Given) => - flip(tp.widen.widenToParents) - case _ => flip(tp) - } - (prepare(tp1) relaxed_<:< prepare(tp2)) || viewExists(tp1, tp2) - } + tp.widen.widenToParents + case _ => + tp + + val tp1p = prepare(tp1) + val tp2p = prepare(tp2) + + if Feature.sourceVersion.isAtMost(SourceVersion.`3.4`) + || oldResolution + || !compareGivens + then + // Intermediate rules: better means specialize, but map all type arguments downwards + // These are enabled for 3.0-3.4, and for all comparisons between old-style implicits, + // and in 3.5-migration when we compare with previous rules. + val flip = new TypeMap: + def apply(t: Type) = t match + case t @ AppliedType(tycon, args) => + def mapArg(arg: Type, tparam: TypeParamInfo) = + if (variance > 0 && tparam.paramVarianceSign < 0) defn.FunctionNOf(arg :: Nil, defn.UnitType) + else arg + mapOver(t.derivedAppliedType(tycon, args.zipWithConserve(tycon.typeParams)(mapArg))) + case _ => mapOver(t) + (flip(tp1p) relaxed_<:< flip(tp2p)) || viewExists(tp1, tp2) + else + // New rules: better means generalize + (tp2p relaxed_<:< tp1p) || viewExists(tp2, tp1) + end isAsGoodValueType /** Widen the result type of synthetic given methods from the implementation class to the * type that's implemented. Example @@ -1809,8 +1836,8 @@ trait Applications extends Compatibility { def compareWithTypes(tp1: Type, tp2: Type) = { val ownerScore = compareOwner(alt1.symbol.maybeOwner, alt2.symbol.maybeOwner) - def winsType1 = isAsSpecific(alt1, tp1, alt2, tp2) - def winsType2 = isAsSpecific(alt2, tp2, alt1, tp1) + val winsType1 = isAsGood(alt1, tp1, alt2, tp2) + def winsType2 = isAsGood(alt2, tp2, alt1, tp1) overload.println(i"compare($alt1, $alt2)? $tp1 $tp2 $ownerScore $winsType1 $winsType2") if winsType1 && winsType2 diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index c4ab7381bf5f..a8fa0fa93b74 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -1105,8 +1105,8 @@ trait Implicits: case result: SearchFailure if result.isAmbiguous => val deepPt = pt.deepenProto if (deepPt ne pt) inferImplicit(deepPt, argument, span) - else if (migrateTo3 && !ctx.mode.is(Mode.OldOverloadingResolution)) - withMode(Mode.OldOverloadingResolution)(inferImplicit(pt, argument, span)) match { + else if (migrateTo3 && !ctx.mode.is(Mode.OldImplicitResolution)) + withMode(Mode.OldImplicitResolution)(inferImplicit(pt, argument, span)) match { case altResult: SearchSuccess => report.migrationWarning( result.reason.msg @@ -1221,7 +1221,7 @@ trait Implicits: assert(argument.isEmpty || argument.tpe.isValueType || argument.tpe.isInstanceOf[ExprType], em"found: $argument: ${argument.tpe}, expected: $pt") - private def nestedContext() = + private def searchContext() = ctx.fresh.setMode(ctx.mode &~ Mode.ImplicitsEnabled) private def isCoherent = pt.isRef(defn.CanEqualClass) @@ -1265,7 +1265,7 @@ trait Implicits: else val history = ctx.searchHistory.nest(cand, pt) val typingCtx = - nestedContext().setNewTyperState().setFreshGADTBounds.setSearchHistory(history) + searchContext().setNewTyperState().setFreshGADTBounds.setSearchHistory(history) val result = typedImplicit(cand, pt, argument, span)(using typingCtx) result match case res: SearchSuccess => @@ -1290,9 +1290,24 @@ trait Implicits: * 0 if neither alternative is preferred over the other */ def compareAlternatives(alt1: RefAndLevel, alt2: RefAndLevel): Int = + def comp(using Context) = explore(compare(alt1.ref, alt2.ref, preferGeneral = true)) if alt1.ref eq alt2.ref then 0 else if alt1.level != alt2.level then alt1.level - alt2.level - else explore(compare(alt1.ref, alt2.ref))(using nestedContext()) + else + val cmp = comp(using searchContext()) + if Feature.sourceVersion == SourceVersion.`3.5-migration` then + val prev = comp(using searchContext().addMode(Mode.OldImplicitResolution)) + if cmp != prev then + def choice(c: Int) = c match + case -1 => "the second alternative" + case 1 => "the first alternative" + case _ => "none - it's ambiguous" + report.warning( + em"""Change in given search preference for $pt between alternatives ${alt1.ref} and ${alt2.ref} + |Previous choice: ${choice(prev)} + |New choice : ${choice(cmp)}""", srcPos) + cmp + end compareAlternatives /** If `alt1` is also a search success, try to disambiguate as follows: * - If alt2 is preferred over alt1, pick alt2, otherwise return an @@ -1328,8 +1343,8 @@ trait Implicits: else ctx.typerState - diff = inContext(ctx.withTyperState(comparisonState)): - compare(ref1, ref2) + diff = inContext(searchContext().withTyperState(comparisonState)): + compare(ref1, ref2, preferGeneral = true) else // alt1 is a conversion, prefer extension alt2 over it diff = -1 if diff < 0 then alt2 diff --git a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala index 33643a0fae2f..5ab6a4a5fae6 100644 --- a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala +++ b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala @@ -296,7 +296,7 @@ trait ImportSuggestions: var i = 0 var diff = 0 while i < filled && diff == 0 do - diff = compare(ref, top(i))(using noImplicitsCtx) + diff = compare(ref, top(i), preferGeneral = true)(using noImplicitsCtx) if diff > 0 then rest += top(i) top(i) = ref diff --git a/docs/_docs/reference/changed-features/implicit-resolution.md b/docs/_docs/reference/changed-features/implicit-resolution.md index 1396ed04b6d3..0df8d2d60a7a 100644 --- a/docs/_docs/reference/changed-features/implicit-resolution.md +++ b/docs/_docs/reference/changed-features/implicit-resolution.md @@ -165,7 +165,22 @@ Condition (*) is new. It is necessary to ensure that the defined relation is tra [//]: # todo: expand with precise rules -**9.** The following change is currently enabled in `-source future`: + +**9.** Given disambiguation has changed. When comparing two givens that both match an expected type, we used to pick the most specific one, in alignment with +overloading resolution. From Scala 3.5 on, we pick the most general one instead. Compiling with Scala 3.5-migration will print a warning in all cases where the preference has changed. Example: +```scala +class A +class B extends A +class C extends A + +given A = A() +given B = B() +given C = C() + +summon[A] // was ambiguous, will now return `given_A` +``` + +**10.** The following change is currently enabled in `-source future`: Implicit resolution now avoids generating recursive givens that can lead to an infinite loop at runtime. Here is an example: diff --git a/tests/neg/i15264.scala b/tests/neg/i15264.scala new file mode 100644 index 000000000000..e13e1089dba3 --- /dev/null +++ b/tests/neg/i15264.scala @@ -0,0 +1,59 @@ +import language.`3.5` +object priority: + // lower number = higher priority + class Prio0 extends Prio1 + object Prio0 { given Prio0() } + + class Prio1 extends Prio2 + object Prio1 { given Prio1() } + + class Prio2 + object Prio2 { given Prio2() } + +object repro: + // analogous to cats Eq, Hash, Order: + class A[V] + class B[V] extends A[V] + class C[V] extends A[V] + + class Q[V] + + object context: + // prios work here, which is cool + given[V](using priority.Prio0): C[V] = new C[V] + given[V](using priority.Prio1): B[V] = new B[V] + given[V](using priority.Prio2): A[V] = new A[V] + + object exports: + // so will these exports + export context.given + + // if you import these don't import from 'context' above + object qcontext: + // base defs, like what you would get from cats + given gb: B[Int] = new B[Int] + given gc: C[Int] = new C[Int] + + // these seem like they should work but don't + given gcq[V](using p0: priority.Prio0)(using c: C[V]): C[Q[V]] = new C[Q[V]] + given gbq[V](using p1: priority.Prio1)(using b: B[V]): B[Q[V]] = new B[Q[V]] + given gaq[V](using p2: priority.Prio2)(using a: A[V]): A[Q[V]] = new A[Q[V]] + +object test1: + import repro.* + import repro.exports.given + + // these will work + val a = summon[A[Int]] + +object test2: + import repro.* + import repro.qcontext.given + + // This one will fail as ambiguous - prios aren't having an effect. + // Priorities indeed don't have an effect if the result is already decided + // without using clauses, they onyl act as a tie breaker. + // With the new resolution rules, it's ambiguous since we pick `gaq` for + // summon, and that needs an A[Int], but there are only the two competing choices + // qb and qc. + val a = summon[A[Q[Int]]] // error: ambiguous between qb and qc for A[Int] diff --git a/tests/pos/i15264.scala b/tests/pos/i15264.scala index 05992df61b94..5be8436c12ba 100644 --- a/tests/pos/i15264.scala +++ b/tests/pos/i15264.scala @@ -30,6 +30,7 @@ object repro: // if you import these don't import from 'context' above object qcontext: // base defs, like what you would get from cats + given ga: A[Int] = new B[Int] // added so that we don't get an ambiguity in test2 given gb: B[Int] = new B[Int] given gc: C[Int] = new C[Int] @@ -45,9 +46,9 @@ object test1: // these will work val a = summon[A[Int]] + object test2: import repro.* import repro.qcontext.given - // this one will fail as ambiguous - prios aren't having an effect - val a = summon[A[Q[Int]]] \ No newline at end of file + val a = summon[A[Q[Int]]] diff --git a/tests/pos/overload-disambiguation.scala b/tests/pos/overload-disambiguation.scala new file mode 100644 index 000000000000..58b085758d92 --- /dev/null +++ b/tests/pos/overload-disambiguation.scala @@ -0,0 +1,13 @@ +class A +class B +class C[-T] + +def foo(using A): C[Any] = ??? +def foo(using B): C[Int] = ??? + + +@main def Test = + given A = A() + given B = B() + val x = foo + val _: C[Any] = x diff --git a/tests/run/given-triangle.check b/tests/run/given-triangle.check new file mode 100644 index 000000000000..5ba9e6a1e8b9 --- /dev/null +++ b/tests/run/given-triangle.check @@ -0,0 +1,3 @@ +class A +class B +class C diff --git a/tests/run/given-triangle.scala b/tests/run/given-triangle.scala new file mode 100644 index 000000000000..5ddba8df8b7b --- /dev/null +++ b/tests/run/given-triangle.scala @@ -0,0 +1,16 @@ +import language.future + +class A +class B extends A +class C extends A + +given A = A() +given B = B() +given C = C() + +def f(using a: A, b: B, c: C) = + println(a.getClass) + println(b.getClass) + println(c.getClass) + +@main def Test = f diff --git a/tests/run/implicit-specifity.scala b/tests/run/implicit-specifity.scala index 51fa02d91cfd..14954eddf2ef 100644 --- a/tests/run/implicit-specifity.scala +++ b/tests/run/implicit-specifity.scala @@ -1,3 +1,5 @@ +import language.`3.5` + case class Show[T](val i: Int) object Show { def apply[T](implicit st: Show[T]): Int = st.i @@ -38,5 +40,5 @@ object Test extends App { assert(Show[Int] == 0) assert(Show[String] == 1) assert(Show[Generic] == 1) // showGen loses against fallback due to longer argument list - assert(Show[Generic2] == 2) // ... but the opaque type intersection trick works. + assert(Show[Generic2] == 1) // ... and the opaque type intersection trick no longer works with new resolution rules. } diff --git a/tests/run/implied-for.scala b/tests/run/implied-for.scala index c7789ce570e4..a55d59e89505 100644 --- a/tests/run/implied-for.scala +++ b/tests/run/implied-for.scala @@ -20,7 +20,7 @@ object Test extends App { val x2: T = t val x3: D[Int] = d - assert(summon[T].isInstanceOf[B]) + assert(summon[T].isInstanceOf[T]) assert(summon[D[Int]].isInstanceOf[D[_]]) } diff --git a/tests/run/implied-priority.scala b/tests/run/implied-priority.scala index 0822fae6778f..61049de8e43e 100644 --- a/tests/run/implied-priority.scala +++ b/tests/run/implied-priority.scala @@ -1,5 +1,6 @@ /* These tests show various mechanisms available for implicit prioritization. */ +import language.`3.5` class E[T](val str: String) // The type for which we infer terms below @@ -72,16 +73,16 @@ def test2a = { } /* If that solution is not applicable, we can define an override by refining the - * result type of the given instance, e.g. like this: + * result type of all lower-priority instances, e.g. like this: */ object Impl3 { - given t1[T]: E[T]("low") + trait LowPriority // A marker trait to indicate a lower priority + given t1[T]: E[T]("low") with LowPriority } object Override { - trait HighestPriority // A marker trait to indicate a higher priority - given over[T]: E[T]("hi") with HighestPriority() + given over[T]: E[T]("hi") with {} } def test3 = { @@ -90,7 +91,7 @@ def test3 = { { import Override.given import Impl3.given - assert(summon[E[String]].str == "hi") // `over` takes priority since its result type is a subtype of t1's. + assert(summon[E[String]].str == "hi", summon[E[String]].str) // `Impl3` takes priority since its result type is a subtype of t1's. } } diff --git a/tests/warn/given-triangle.check b/tests/warn/given-triangle.check new file mode 100644 index 000000000000..69583830c2bc --- /dev/null +++ b/tests/warn/given-triangle.check @@ -0,0 +1,6 @@ +-- Warning: tests/warn/given-triangle.scala:16:18 ---------------------------------------------------------------------- +16 |@main def Test = f // warn + | ^ + | Change in given search preference for A between alternatives (given_A : A) and (given_B : B) + | Previous choice: the second alternative + | New choice : the first alternative diff --git a/tests/warn/given-triangle.scala b/tests/warn/given-triangle.scala new file mode 100644 index 000000000000..bc1a5c774f4f --- /dev/null +++ b/tests/warn/given-triangle.scala @@ -0,0 +1,16 @@ +//> using options -source 3.5-migration + +class A +class B extends A +class C extends A + +given A = A() +given B = B() +given C = C() + +def f(using a: A, b: B, c: C) = + println(a.getClass) + println(b.getClass) + println(c.getClass) + +@main def Test = f // warn From fd97d68639f350e2f99b28c65a0c106cf629502e Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 4 Mar 2024 21:26:31 +0100 Subject: [PATCH 59/63] Allow ContextBoundParamNames to be unmangled. Also, fix the unmangling of UniqueExtNames, which seemingly never worked. --- .../src/dotty/tools/dotc/core/NameKinds.scala | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/NameKinds.scala b/compiler/src/dotty/tools/dotc/core/NameKinds.scala index d4f009cbbbd5..74d440562824 100644 --- a/compiler/src/dotty/tools/dotc/core/NameKinds.scala +++ b/compiler/src/dotty/tools/dotc/core/NameKinds.scala @@ -182,13 +182,13 @@ object NameKinds { case DerivedName(underlying, info: this.NumberedInfo) => Some((underlying, info.num)) case _ => None } - protected def skipSeparatorAndNum(name: SimpleName, separator: String): Int = { + protected def skipSeparatorAndNum(name: SimpleName, separator: String): Int = var i = name.length - while (i > 0 && name(i - 1).isDigit) i -= 1 - if (i > separator.length && i < name.length && - name.slice(i - separator.length, i).toString == separator) i + while i > 0 && name(i - 1).isDigit do i -= 1 + if i >= separator.length && i < name.length + && name.slice(i - separator.length, i).toString == separator + then i else -1 - } numberedNameKinds(tag) = this: @unchecked } @@ -240,6 +240,16 @@ object NameKinds { } } + /** Unique names that can be unmangled */ + class UniqueNameKindWithUnmangle(separator: String) extends UniqueNameKind(separator): + override def unmangle(name: SimpleName): TermName = + val i = skipSeparatorAndNum(name, separator) + if i > 0 then + val index = name.drop(i).toString.toInt + val original = name.take(i - separator.length).asTermName + apply(original, index) + else name + /** Names of the form `prefix . name` */ val QualifiedName: QualifiedNameKind = new QualifiedNameKind(QUALIFIED, ".") @@ -288,7 +298,7 @@ object NameKinds { * * The "evidence$" prefix is a convention copied from Scala 2. */ - val ContextBoundParamName: UniqueNameKind = new UniqueNameKind("evidence$") + val ContextBoundParamName: UniqueNameKind = new UniqueNameKindWithUnmangle("evidence$") /** The name of an inferred contextual function parameter: * @@ -323,20 +333,7 @@ object NameKinds { val InlineBinderName: UniqueNameKind = new UniqueNameKind("$proxy") val MacroNames: UniqueNameKind = new UniqueNameKind("$macro$") - /** A kind of unique extension methods; Unlike other unique names, these can be - * unmangled. - */ - val UniqueExtMethName: UniqueNameKind = new UniqueNameKind("$extension") { - override def unmangle(name: SimpleName): TermName = { - val i = skipSeparatorAndNum(name, separator) - if (i > 0) { - val index = name.drop(i).toString.toInt - val original = name.take(i - separator.length).asTermName - apply(original, index) - } - else name - } - } + val UniqueExtMethName: UniqueNameKind = new UniqueNameKindWithUnmangle("$extension") /** Kinds of unique names generated by the pattern matcher */ val PatMatStdBinderName: UniqueNameKind = new UniqueNameKind("x") From 495072aac43cc5f9026858d0561cf87808877409 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 8 Mar 2024 16:26:49 +0100 Subject: [PATCH 60/63] Implement context bound companions --- .../src/dotty/tools/dotc/ast/Desugar.scala | 63 +++++++++------- .../src/dotty/tools/dotc/ast/TreeInfo.scala | 31 ++++++++ .../dotty/tools/dotc/core/Definitions.scala | 9 +++ .../src/dotty/tools/dotc/core/NamerOps.scala | 59 +++++++++++++++ .../src/dotty/tools/dotc/core/StdNames.scala | 2 + .../tools/dotc/core/SymDenotations.scala | 4 +- .../src/dotty/tools/dotc/core/SymUtils.scala | 3 + .../tools/dotc/printing/RefinedPrinter.scala | 2 +- .../tools/dotc/reporting/ErrorMessageID.scala | 2 + .../dotty/tools/dotc/reporting/messages.scala | 36 +++++++++ .../tools/dotc/transform/PostTyper.scala | 22 ++++-- .../tools/dotc/transform/TreeChecker.scala | 21 +++--- .../src/dotty/tools/dotc/typer/Namer.scala | 20 +++++ .../src/dotty/tools/dotc/typer/Typer.scala | 73 +++++++++++++++++++ .../annotation/internal/WitnessNames.scala | 53 ++++++++++++++ tests/neg/FromString.scala | 12 --- tests/neg/cb-companion-leaks.check | 66 +++++++++++++++++ tests/neg/cb-companion-leaks.scala | 18 +++++ tests/new/test.scala | 23 +++++- tests/pos-macros/i8325/Macro_1.scala | 4 +- tests/pos-macros/i8325/Test_2.scala | 2 +- tests/pos-macros/i8325b/Macro_1.scala | 4 +- tests/pos-macros/i8325b/Test_2.scala | 2 +- tests/pos/FromString.scala | 7 +- tests/pos/cb-companion-joins.scala | 23 ++++++ tests/run/given-disambiguation.scala | 58 +++++++++++++++ 26 files changed, 552 insertions(+), 67 deletions(-) create mode 100644 library/src/scala/annotation/internal/WitnessNames.scala delete mode 100644 tests/neg/FromString.scala create mode 100644 tests/neg/cb-companion-leaks.check create mode 100644 tests/neg/cb-companion-leaks.scala create mode 100644 tests/pos/cb-companion-joins.scala create mode 100644 tests/run/given-disambiguation.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 674d66d29b35..0dc5ad721d77 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -227,30 +227,41 @@ object desugar { addDefaultGetters(elimContextBounds(meth, isPrimaryConstructor)) private def desugarContextBounds( - tname: TypeName, rhs: Tree, + tdef: TypeDef, evidenceBuf: ListBuffer[ValDef], flags: FlagSet, - freshName: => TermName)(using Context): Tree = rhs match - case ContextBounds(tbounds, cxbounds) => - val isMember = flags.isAllOf(DeferredGivenFlags) - for bound <- cxbounds do - val evidenceName = bound match - case ContextBoundTypeTree(_, _, ownName) if !ownName.isEmpty => - ownName - case _ if !isMember && cxbounds.tail.isEmpty && Feature.enabled(Feature.modularity) => - tname.toTermName - case _ => - if isMember then inventGivenOrExtensionName(bound) - else freshName - val evidenceParam = ValDef(evidenceName, bound, EmptyTree).withFlags(flags) - evidenceParam.pushAttachment(ContextBoundParam, ()) - evidenceBuf += evidenceParam - tbounds - case LambdaTypeTree(tparams, body) => - cpy.LambdaTypeTree(rhs)(tparams, - desugarContextBounds(tname, body, evidenceBuf, flags, freshName)) - case _ => - rhs + freshName: => TermName)(using Context): TypeDef = + + val evidenceNames = ListBuffer[TermName]() + + def desugarRhs(rhs: Tree): Tree = rhs match + case ContextBounds(tbounds, cxbounds) => + val isMember = flags.isAllOf(DeferredGivenFlags) + for bound <- cxbounds do + val evidenceName = bound match + case ContextBoundTypeTree(_, _, ownName) if !ownName.isEmpty => + ownName + case _ if !isMember && cxbounds.tail.isEmpty && Feature.enabled(Feature.modularity) => + tdef.name.toTermName + case _ => + if isMember then inventGivenOrExtensionName(bound) + else freshName + evidenceNames += evidenceName + val evidenceParam = ValDef(evidenceName, bound, EmptyTree).withFlags(flags) + evidenceParam.pushAttachment(ContextBoundParam, ()) + evidenceBuf += evidenceParam + tbounds + case LambdaTypeTree(tparams, body) => + cpy.LambdaTypeTree(rhs)(tparams, desugarRhs(body)) + case _ => + rhs + + val tdef1 = cpy.TypeDef(tdef)(rhs = desugarRhs(tdef.rhs)) + if evidenceNames.nonEmpty && !evidenceNames.contains(tdef.name.toTermName) then + val witnessNamesAnnot = WitnessNamesAnnot(evidenceNames.toList).withSpan(tdef.span) + tdef1.withAddedAnnotation(witnessNamesAnnot) + else + tdef1 end desugarContextBounds private def elimContextBounds(meth: DefDef, isPrimaryConstructor: Boolean)(using Context): DefDef = @@ -269,8 +280,7 @@ object desugar { val iflag = if Feature.sourceVersion.isAtLeast(`future`) then Given else Implicit val flags = if isPrimaryConstructor then iflag | LocalParamAccessor else iflag | Param mapParamss(paramss) { - tparam => cpy.TypeDef(tparam)(rhs = - desugarContextBounds(tparam.name, tparam.rhs, evidenceParamBuf, flags, freshName)) + tparam => desugarContextBounds(tparam, evidenceParamBuf, flags, freshName) }(identity) rhs match @@ -485,9 +495,8 @@ object desugar { def typeDef(tdef: TypeDef)(using Context): Tree = val evidenceBuf = new ListBuffer[ValDef] - val result = cpy.TypeDef(tdef)(rhs = - desugarContextBounds(tdef.name, tdef.rhs, evidenceBuf, - (tdef.mods.flags.toTermFlags & AccessFlags) | Lazy | DeferredGivenFlags, EmptyTermName)) + val result = desugarContextBounds(tdef, evidenceBuf, + (tdef.mods.flags.toTermFlags & AccessFlags) | Lazy | DeferredGivenFlags, EmptyTermName) if evidenceBuf.isEmpty then result else Thicket(result :: evidenceBuf.toList) /** The expansion of a class definition. See inline comments for what is involved */ diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index 28d3ef6daaef..21f1a44220cf 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -5,6 +5,8 @@ package ast import core.* import Flags.*, Trees.*, Types.*, Contexts.* import Names.*, StdNames.*, NameOps.*, Symbols.* +import Annotations.Annotation +import NameKinds.ContextBoundParamName import typer.ConstFold import reporting.trace @@ -376,6 +378,35 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] => case _ => tree.tpe.isInstanceOf[ThisType] } + + /** Extractor for annotation.internal.WitnessNames(name_1, ..., name_n)` + * represented as an untyped or typed tree. + */ + object WitnessNamesAnnot: + def apply(names0: List[TermName])(using Context): untpd.Tree = + untpd.TypedSplice(tpd.New( + defn.WitnessNamesAnnot.typeRef, + tpd.SeqLiteral(names0.map(n => tpd.Literal(Constant(n.toString))), tpd.TypeTree(defn.StringType)) :: Nil + )) + + def unapply(tree: Tree)(using Context): Option[List[TermName]] = + def isWitnessNames(tp: Type) = tp match + case tp: TypeRef => + tp.name == tpnme.WitnessNames && tp.symbol == defn.WitnessNamesAnnot + case _ => + false + unsplice(tree) match + case Apply( + Select(New(tpt: tpd.TypeTree), nme.CONSTRUCTOR), + SeqLiteral(elems, _) :: Nil + ) if isWitnessNames(tpt.tpe) => + Some: + elems.map: + case Literal(Constant(str: String)) => + ContextBoundParamName.unmangle(str.toTermName.asSimpleName) + case _ => + None + end WitnessNamesAnnot } trait UntypedTreeInfo extends TreeInfo[Untyped] { self: Trees.Instance[Untyped] => diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index ca32cc0d3433..9740a0568b39 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -459,6 +459,13 @@ class Definitions { @tu lazy val andType: TypeSymbol = enterBinaryAlias(tpnme.AND, AndType(_, _)) @tu lazy val orType: TypeSymbol = enterBinaryAlias(tpnme.OR, OrType(_, _, soft = false)) + @tu lazy val CBCompanion: TypeSymbol = // type ``[-Refs] + enterPermanentSymbol(tpnme.CBCompanion, + TypeBounds(NothingType, + HKTypeLambda(tpnme.syntheticTypeParamName(0) :: Nil, Contravariant :: Nil)( + tl => TypeBounds.empty :: Nil, + tl => AnyType))).asType + /** Method representing a throw */ @tu lazy val throwMethod: TermSymbol = enterMethod(OpsPackageClass, nme.THROWkw, MethodType(List(ThrowableType), NothingType)) @@ -1064,6 +1071,7 @@ class Definitions { @tu lazy val RetainsCapAnnot: ClassSymbol = requiredClass("scala.annotation.retainsCap") @tu lazy val RetainsByNameAnnot: ClassSymbol = requiredClass("scala.annotation.retainsByName") @tu lazy val PublicInBinaryAnnot: ClassSymbol = requiredClass("scala.annotation.publicInBinary") + @tu lazy val WitnessNamesAnnot: ClassSymbol = requiredClass("scala.annotation.internal.WitnessNames") @tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable") @@ -2141,6 +2149,7 @@ class Definitions { NullClass, NothingClass, SingletonClass, + CBCompanion, MaybeCapabilityAnnot) @tu lazy val syntheticCoreClasses: List[Symbol] = syntheticScalaClasses ++ List( diff --git a/compiler/src/dotty/tools/dotc/core/NamerOps.scala b/compiler/src/dotty/tools/dotc/core/NamerOps.scala index b791088914ea..347b51c01a64 100644 --- a/compiler/src/dotty/tools/dotc/core/NamerOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NamerOps.scala @@ -4,8 +4,10 @@ package core import Contexts.*, Symbols.*, Types.*, Flags.*, Scopes.*, Decorators.*, Names.*, NameOps.* import SymDenotations.{LazyType, SymDenotation}, StdNames.nme +import ContextOps.enter import TypeApplications.EtaExpansion import collection.mutable +import config.Printers.typr /** Operations that are shared between Namer and TreeUnpickler */ object NamerOps: @@ -248,4 +250,61 @@ object NamerOps: rhsCtx.gadtState.addBound(psym, tr, isUpper = true) } + /** Create a context-bound companion for type symbol `tsym` unless it + * would clash with another parameter. `tsym` is a context-bound symbol + * that defined a set of witnesses with names `witnessNames`. + * + * @param paramSymss If `tsym` is a type parameter, the other parameter symbols, + * including witnesses, of the method containing `tsym`. + * If `tsym` is an abstract type member, `paramSymss` is the + * empty list. + * + * The context-bound companion has as name the name of `tsym` translated to + * a term name. We create a synthetic val of the form + * + * val A: CBCompanion[witnessRef1 | ... | witnessRefN] + * + * where + * + * CBCompanion is the type created in Definitions + * withnessRefK is a refence to the K'the witness. + * + * The companion has the same access flags as the original type. + */ + def maybeAddContextBoundCompanionFor(tsym: Symbol, witnessNames: List[TermName], paramSymss: List[List[Symbol]])(using Context): Unit = + val prefix = ctx.owner.thisType + val companionName = tsym.name.toTermName + val witnessRefs = + if paramSymss.nonEmpty then + if paramSymss.nestedExists(_.name == companionName) then Nil + else + witnessNames.map: witnessName => + prefix.select(paramSymss.nestedFind(_.name == witnessName).get) + else + witnessNames.map(prefix.select) + if witnessRefs.nonEmpty then + val cbtype = defn.CBCompanion.typeRef.appliedTo: + witnessRefs.reduce[Type](OrType(_, _, soft = false)) + val cbc = newSymbol( + ctx.owner, companionName, + (tsym.flagsUNSAFE & AccessFlags) | Synthetic, + cbtype) + typr.println(i"contetx bpund companion created $cbc: $cbtype in ${ctx.owner}") + ctx.enter(cbc) + end maybeAddContextBoundCompanionFor + + /** Add context bound companions to all context-bound types declared in + * this class. This assumes that these types already have their + * WitnessNames annotation set even before they are completed. This is + * the case for unpickling but currently not for Namer. So the method + * is only called during unpickling, and is not part of NamerOps. + */ + def addContextBoundCompanions(cls: ClassSymbol)(using Context): Unit = + for sym <- cls.info.decls do + if sym.isType && !sym.isClass then + for ann <- sym.annotationsUNSAFE do + if ann.symbol == defn.WitnessNamesAnnot then + ann.tree match + case ast.tpd.WitnessNamesAnnot(witnessNames) => + maybeAddContextBoundCompanionFor(sym, witnessNames, Nil) end NamerOps diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index dde7f7149055..d819bdaeeb6e 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -288,6 +288,7 @@ object StdNames { // Compiler-internal val CAPTURE_ROOT: N = "cap" + val CBCompanion: N = "" val CONSTRUCTOR: N = "" val STATIC_CONSTRUCTOR: N = "" val EVT2U: N = "evt2u$" @@ -394,6 +395,7 @@ object StdNames { val TypeApply: N = "TypeApply" val TypeRef: N = "TypeRef" val UNIT : N = "UNIT" + val WitnessNames: N = "WitnessNames" val acc: N = "acc" val adhocExtensions: N = "adhocExtensions" val andThen: N = "andThen" diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 55f7b3f4b22c..78637bc20727 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -1207,8 +1207,8 @@ object SymDenotations { */ final def isEffectivelySealed(using Context): Boolean = isOneOf(FinalOrSealed) - || isClass && (!isOneOf(EffectivelyOpenFlags) - || isLocalToCompilationUnit) + || isClass + && (!isOneOf(EffectivelyOpenFlags) || isLocalToCompilationUnit) final def isLocalToCompilationUnit(using Context): Boolean = is(Private) diff --git a/compiler/src/dotty/tools/dotc/core/SymUtils.scala b/compiler/src/dotty/tools/dotc/core/SymUtils.scala index 65634241b790..ec2da3cbb4ef 100644 --- a/compiler/src/dotty/tools/dotc/core/SymUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/SymUtils.scala @@ -87,6 +87,9 @@ class SymUtils: !d.isPrimitiveValueClass } + def isContextBoundCompanion(using Context): Boolean = + self.is(Synthetic) && self.info.typeSymbol == defn.CBCompanion + /** Is this a case class for which a product mirror is generated? * Excluded are value classes, abstract classes and case classes with more than one * parameter section. diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 0658b4f6d932..01765cd95388 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -640,7 +640,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { def toTextAnnot = toTextLocal(arg) ~~ annotText(annot.symbol.enclosingClass, annot) def toTextRetainsAnnot = - try changePrec(GlobalPrec)(toText(arg) ~ "^" ~ toTextCaptureSet(captureSet)) + try changePrec(GlobalPrec)(toTextLocal(arg) ~ "^" ~ toTextCaptureSet(captureSet)) catch case ex: IllegalCaptureRef => toTextAnnot if annot.symbol.maybeOwner.isRetains && Feature.ccEnabled && !printDebug diff --git a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala index 6011587a7100..fe0778d5eadb 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -207,6 +207,8 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe case MatchTypeLegacyPatternID // errorNumber: 191 case UnstableInlineAccessorID // errorNumber: 192 case VolatileOnValID // errorNumber: 193 + case ConstructorProxyNotValueID // errorNumber: 194 + case ContextBoundCompanionNotValueID // errorNumber: 195 def errorNumber = ordinal - 1 diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 484bc88c0983..e0d9d06bdee6 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -3159,3 +3159,39 @@ class VolatileOnVal()(using Context) extends SyntaxMsg(VolatileOnValID): protected def msg(using Context): String = "values cannot be volatile" protected def explain(using Context): String = "" + +class ConstructorProxyNotValue(sym: Symbol)(using Context) +extends TypeMsg(ConstructorProxyNotValueID): + protected def msg(using Context): String = + i"constructor proxy $sym cannot be used as a value" + protected def explain(using Context): String = + i"""A constructor proxy is a symbol made up by the compiler to represent a non-existent + |factory method of a class. For instance, in + | + | class C(x: Int) + | + |C does not have an apply method since it is not a case class. Yet one can + |still create instances with applications like `C(3)` which expand to `new C(3)`. + |The `C` in this call is a constructor proxy. It can only be used as applications + |but not as a stand-alone value.""" + +class ContextBoundCompanionNotValue(sym: Symbol)(using Context) +extends TypeMsg(ConstructorProxyNotValueID): + protected def msg(using Context): String = + i"context bound companion $sym cannot be used as a value" + protected def explain(using Context): String = + i"""A context bound companion is a symbol made up by the compiler to represent the + |witness or witnesses generated for the context bound(s) of a type parameter or type. + |For instance, in + | + | class Monoid extends SemiGroup: + | type Self + | def unit: Self + | + | type A: Monoid + | + |there is just a type `A` declared but not a value `A`. Nevertheless, one can write + |the selection `A.unit`, which works because the compiler created a context bound + |companion value with the (term-)name `A`. However, these context bound companions + |are not values themselves, they can only be referred to in selections.""" + diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 5c86591280f7..62f188320f2c 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -261,9 +261,13 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => } } - def checkNoConstructorProxy(tree: Tree)(using Context): Unit = + def checkUsableAsValue(tree: Tree)(using Context): Unit = + def unusable(msg: Symbol => Message) = + report.error(msg(tree.symbol), tree.srcPos) if tree.symbol.is(ConstructorProxy) then - report.error(em"constructor proxy ${tree.symbol} cannot be used as a value", tree.srcPos) + unusable(ConstructorProxyNotValue(_)) + if tree.symbol.isContextBoundCompanion then + unusable(ContextBoundCompanionNotValue(_)) def checkStableSelection(tree: Tree)(using Context): Unit = def check(qual: Tree) = @@ -293,7 +297,7 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => if tree.isType then checkNotPackage(tree) else - checkNoConstructorProxy(tree) + checkUsableAsValue(tree) registerNeedsInlining(tree) tree.tpe match { case tpe: ThisType => This(tpe.cls).withSpan(tree.span) @@ -305,7 +309,7 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => Checking.checkRealizable(qual.tpe, qual.srcPos) withMode(Mode.Type)(super.transform(checkNotPackage(tree))) else - checkNoConstructorProxy(tree) + checkUsableAsValue(tree) transformSelect(tree, Nil) case tree: Apply => val methType = tree.fun.tpe.widen.asInstanceOf[MethodType] @@ -437,8 +441,14 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => val relativePath = util.SourceFile.relativePath(ctx.compilationUnit.source, reference) sym.addAnnotation(Annotation(defn.SourceFileAnnot, Literal(Constants.Constant(relativePath)), tree.span)) else - if !sym.is(Param) && !sym.owner.isOneOf(AbstractOrTrait) then - Checking.checkGoodBounds(tree.symbol) + if !sym.is(Param) then + if !sym.owner.isOneOf(AbstractOrTrait) then + Checking.checkGoodBounds(tree.symbol) + if sym.owner.isClass && sym.hasAnnotation(defn.WitnessNamesAnnot) then + val decls = sym.owner.info.decls + for cbCompanion <- decls.lookupAll(sym.name.toTermName) do + if cbCompanion.isContextBoundCompanion then + decls.openForMutations.unlink(cbCompanion) (tree.rhs, sym.info) match case (rhs: LambdaTypeTree, bounds: TypeBounds) => VarianceChecker.checkLambda(rhs, bounds) diff --git a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala index 4a7548f40f43..dea2b3ce0769 100644 --- a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala @@ -310,9 +310,11 @@ object TreeChecker { def assertDefined(tree: untpd.Tree)(using Context): Unit = if (tree.symbol.maybeOwner.isTerm) { val sym = tree.symbol + def isAllowed = // constructor proxies and context bound companions are flagged at PostTyper + isSymWithoutDef(sym) && ctx.phase.id < postTyperPhase.id assert( - nowDefinedSyms.contains(sym) || patBoundSyms.contains(sym), - i"undefined symbol ${sym} at line " + tree.srcPos.line + nowDefinedSyms.contains(sym) || patBoundSyms.contains(sym) || isAllowed, + i"undefined symbol ${sym} in ${sym.owner} at line " + tree.srcPos.line ) if (!ctx.phase.patternTranslated) @@ -383,6 +385,9 @@ object TreeChecker { case _ => } + def isSymWithoutDef(sym: Symbol)(using Context): Boolean = + sym.is(ConstructorProxy) || sym.isContextBoundCompanion + /** Exclude from double definition checks any erased symbols that were * made `private` in phase `UnlinkErasedDecls`. These symbols will be removed * completely in phase `Erasure` if they are defined in a currently compiled unit. @@ -609,14 +614,12 @@ object TreeChecker { val decls = cls.classInfo.decls.toList.toSet.filter(isNonMagicalMember) val defined = impl.body.map(_.symbol) - def isAllowed(sym: Symbol): Boolean = sym.is(ConstructorProxy) - - val symbolsNotDefined = (decls -- defined - constr.symbol).filterNot(isAllowed) + val symbolsMissingDefs = (decls -- defined - constr.symbol).filterNot(isSymWithoutDef) - assert(symbolsNotDefined.isEmpty, - i" $cls tree does not define members: ${symbolsNotDefined.toList}%, %\n" + - i"expected: ${decls.toList}%, %\n" + - i"defined: ${defined}%, %") + assert(symbolsMissingDefs.isEmpty, + i"""$cls tree does not define members: ${symbolsMissingDefs.toList}%, % + |expected: ${decls.toList}%, % + |defined: ${defined}%, %""") super.typedClassDef(cdef, cls) } diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 38f4b10e0a69..0c84fa53c225 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -406,6 +406,7 @@ class Namer { typer: Typer => enterSymbol(sym) setDocstring(sym, origStat) addEnumConstants(mdef, sym) + maybeAddCBCompanion(mdef, Nil) ctx case stats: Thicket => stats.toList.foreach(recur) @@ -1875,6 +1876,10 @@ class Namer { typer: Typer => val paramSymss = normalizeIfConstructor(ddef.paramss.nestedMap(symbolOfTree), isConstructor) sym.setParamss(paramSymss) + if !ddef.name.is(DefaultGetterName) && !sym.is(Synthetic) then + for params <- ddef.paramss; param <- params do + maybeAddCBCompanion(param, paramSymss) + /** Set every context bound evidence parameter of a class to be tracked, * provided it has a type that has an abstract type member and the parameter's * name is not a synthetic, unaddressable name. Reset private and local flags @@ -2067,6 +2072,21 @@ class Namer { typer: Typer => } end inferredResultType + /** If `mdef` is a TypeDef with a @WitnessNames annotation, add a context bound + * companion, provided `mdef` is a paremeter exactly when paramSymss is non empty. + * maybeAddCBCompanion is called in two places: + * - when analyzing a TypeDef where we want to add a companion if it is + * a member type. In this case the TypeDef is not a parameter and paramSymss is empty. + * - when analyzing a DefDef and traversing all its type parameters. + * In this case the TypeDef is a parameter and paramSymss is non-empty. + */ + def maybeAddCBCompanion(mdef: DefTree, paramSymss: List[List[Symbol]])(using Context): Unit = + mdef match + case tdef: TypeDef if mdef.mods.is(Param) == paramSymss.nonEmpty => + for case WitnessNamesAnnot(witnessNames) <- tdef.mods.annotations do + maybeAddContextBoundCompanionFor(symbolOfTree(tdef), witnessNames, paramSymss) + case _ => + /** Prepare a GADT-aware context used to type the RHS of a ValOrDefDef. */ def prepareRhsCtx(rhsCtx: FreshContext, paramss: List[List[Symbol]])(using Context): FreshContext = val typeParams = paramss.collect { case TypeSymbols(tparams) => tparams }.flatten diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 39a82dc78ce2..db2924ce3c13 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -762,6 +762,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer else typedDynamicSelect(tree2, Nil, pt) else + if qual.tpe.typeSymbol == defn.CBCompanion then + val witnessSelection = typedCBSelect(tree0, pt, qual) + if !witnessSelection.isEmpty then return witnessSelection assignType(tree, rawType match case rawType: NamedType => @@ -770,6 +773,76 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer notAMemberErrorType(tree, qual, pt)) end typedSelect + /** Expand a selection A.m on a context bound companion A with type + * `[ref_1 | ... | ref_N]` as described by + * Step 3 of the doc comment of annotation.internal.WitnessNames. + * @return the best alternative if it exists, + * or EmptyTree if no witness admits selecting with the given name, + * or EmptyTree and report an ambiguity error of there are several + * possible witnesses and no selection is better than the other + * according to the critera given in Step 3. + */ + def typedCBSelect(tree: untpd.Select, pt: Type, qual: Tree)(using Context): Tree = + + type Alts = List[(/*prev: */Tree, /*prevState: */TyperState, /*prevWitness: */TermRef)] + + /** Compare two alternative selections `alt1` and `alt2` from witness types + * `wit1`, `wit2` according to the 3 criteria in the enclosing doc comment. I.e. + * + * alt1 = qual1.m, alt2 = qual2.m, qual1: wit1, qual2: wit2 + * + * @return 1 if 1st alternative is preferred over 2nd + * -1 if 2nd alternative is preferred over 1st + * 0 if neither alternative is preferred over the other + */ + def compareAlts(alt1: Tree, alt2: Tree, wit1: TermRef, wit2: TermRef): Int = + val cmpPrefix = compare(wit1, wit2, preferGeneral = true) + typr.println(i"compare witnesses $wit1: ${wit1.info}, $wit2: ${wit2.info} = $cmpPrefix") + if cmpPrefix != 0 then cmpPrefix + else (alt1.tpe, alt2.tpe) match + case (tp1: TypeRef, tp2: TypeRef) => + if tp1.dealias == tp2.dealias then 1 else 0 + case (tp1: TermRef, tp2: TermRef) => + if tp1.info.isSingleton && (tp1 frozen_=:= tp2) then 1 + else compare(tp1, tp2, preferGeneral = false) + case (tp1: TermRef, _) => 1 + case (_, tp2: TermRef) => -1 + case _ => 0 + + /** Find the set of maximally preferred alternative among `prev` and the + * remaining alternatives generated from `witnesses` with is a union type + * of witness references. + */ + def tryAlts(prevs: Alts, witnesses: Type): Alts = witnesses match + case OrType(wit1, wit2) => + tryAlts(tryAlts(prevs, wit1), wit2) + case witness: TermRef => + val altQual = tpd.ref(witness).withSpan(qual.span) + val altCtx = ctx.fresh.setNewTyperState() + val alt = typedSelect(tree, pt, altQual)(using altCtx) + def current = (alt, altCtx.typerState, witness) + if altCtx.reporter.hasErrors then prevs + else + val cmps = prevs.map: (prevTree, prevState, prevWitness) => + compareAlts(prevTree, alt, prevWitness, witness) + if cmps.exists(_ == 1) then prevs + else current :: prevs.zip(cmps).collect{ case (prev, cmp) if cmp != -1 => prev } + + qual.tpe.widen match + case AppliedType(_, arg :: Nil) => + tryAlts(Nil, arg) match + case Nil => EmptyTree + case (best @ (bestTree, bestState, _)) :: Nil => + bestState.commit() + bestTree + case multiAlts => + report.error( + em"""Ambiguous witness reference. None of the following alternatives is more specific than the other: + |${multiAlts.map((alt, _, witness) => i"\n $witness.${tree.name}: ${alt.tpe.widen}")}""", + tree.srcPos) + EmptyTree + end typedCBSelect + def typedSelect(tree: untpd.Select, pt: Type)(using Context): Tree = { record("typedSelect") diff --git a/library/src/scala/annotation/internal/WitnessNames.scala b/library/src/scala/annotation/internal/WitnessNames.scala new file mode 100644 index 000000000000..f859cda96d06 --- /dev/null +++ b/library/src/scala/annotation/internal/WitnessNames.scala @@ -0,0 +1,53 @@ +package scala.annotation +package internal + +/** An annotation that is used for marking type definitions that should get + * context bound companions. The scheme is as follows: + * + * 1. When desugaring a context-bounded type A, add a @WitnessNames(n_1, ... , n_k) + * annotation to the type declaration node, where n_1, ..., n_k are the names of + * all the witnesses generated for the context bounds of A. This annotation will + * be pickled as usual. + * + * 2. During Namer or Unpickling, when encountering a type declaration A with + * a WitnessNames(n_1, ... , n_k) annotation, create a CB companion `val A` with + * rtype ``[ref_1 | ... | ref_k] where ref_i is a TermRef + * with the same prefix as A and name n_i. Except, don't do this if the type in + * question is a type parameter and there is already a term parameter with name A + * defined for the same method. + * + * ContextBoundCompanion is defined as an internal abstract type like this: + * + * type ``[-Refs] + * + * The context bound companion's variance is negative, so that unons in the + * arguments are joined when encountering multiple definfitions and forming a glb. + * + * 3. Add a special case for typing a selection A.m on a value A of type + * ContextBoundCompanion[ref_1, ..., ref_k]. Namely, try to typecheck all + * selections ref_1.m, ..., ref_k.m with the expected type. There must be + * a unique selection ref_i.m that typechecks and such that for all other + * selections ref_j.m that also typecheck one of the following three criteria + * applies: + * + * 1. ref_i.m and ref_j.m are the same. This means: If they are types then + * ref_i.m is an alias of ref_j.m. If they are terms then they are both + * singleton types and ref_i.m =:= ref_j.m. + * 2. The underlying type (under widen) of ref_i is a true supertype of the + * underlying type of ref_j. + * 3. ref_i.m is a term, the underlying type of ref_j is not a strict subtype + * of the underlying type of ref_j, and the underlying type ref_i.m is a + * strict subtype of the underlying type of ref_j.m. + * + * If there is such a selection, map A.m to ref_i.m, otherwise report an error. + * + * (2) might surprise. It is the analogue of given disambiguation, where we also + * pick the most general candidate that matches the expected type. E.g. we have + * context bounds for Functor, Monad, and Applicable. In this case we want to + * select the `map` method of `Functor`. + * + * 4. At PostTyper, issue an error when encountering any reference to a CB companion. + */ +class WitnessNames(names: String*) extends StaticAnnotation + + diff --git a/tests/neg/FromString.scala b/tests/neg/FromString.scala deleted file mode 100644 index b81c72aec530..000000000000 --- a/tests/neg/FromString.scala +++ /dev/null @@ -1,12 +0,0 @@ -//> using options -language:experimental.modularity -source future - -trait FromString: - type Self - def fromString(s: String): Self - -given Int forms FromString = _.toInt - -given Double forms FromString = _.toDouble - -def add[N: {FromString, Numeric as num}](a: String, b: String): N = - num.plus(N.fromString(a), N.fromString(b)) // error: Not found: N // error diff --git a/tests/neg/cb-companion-leaks.check b/tests/neg/cb-companion-leaks.check new file mode 100644 index 000000000000..b865d058f0e7 --- /dev/null +++ b/tests/neg/cb-companion-leaks.check @@ -0,0 +1,66 @@ +-- [E194] Type Error: tests/neg/cb-companion-leaks.scala:11:23 --------------------------------------------------------- +11 | def foo[A: {C, D}] = A // error + | ^ + | context bound companion value A cannot be used as a value + |-------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | A context bound companion is a symbol made up by the compiler to represent the + | witness or witnesses generated for the context bound(s) of a type parameter or type. + | For instance, in + | + | class Monoid extends SemiGroup: + | type Self + | def unit: Self + | + | type A: Monoid + | + | there is just a type `A` declared but not a value `A`. Nevertheless, one can write + | the selection `A.unit`, which works because the compiler created a context bound + | companion value with the (term-)name `A`. However, these context bound companions + | are not values themselves, they can only be referred to in selections. + -------------------------------------------------------------------------------------------------------------------- +-- [E194] Type Error: tests/neg/cb-companion-leaks.scala:15:10 --------------------------------------------------------- +15 | val x = A // error + | ^ + | context bound companion value A cannot be used as a value + |-------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | A context bound companion is a symbol made up by the compiler to represent the + | witness or witnesses generated for the context bound(s) of a type parameter or type. + | For instance, in + | + | class Monoid extends SemiGroup: + | type Self + | def unit: Self + | + | type A: Monoid + | + | there is just a type `A` declared but not a value `A`. Nevertheless, one can write + | the selection `A.unit`, which works because the compiler created a context bound + | companion value with the (term-)name `A`. However, these context bound companions + | are not values themselves, they can only be referred to in selections. + -------------------------------------------------------------------------------------------------------------------- +-- [E194] Type Error: tests/neg/cb-companion-leaks.scala:17:9 ---------------------------------------------------------- +17 | val y: A.type = ??? // error + | ^ + | context bound companion value A cannot be used as a value + |-------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | A context bound companion is a symbol made up by the compiler to represent the + | witness or witnesses generated for the context bound(s) of a type parameter or type. + | For instance, in + | + | class Monoid extends SemiGroup: + | type Self + | def unit: Self + | + | type A: Monoid + | + | there is just a type `A` declared but not a value `A`. Nevertheless, one can write + | the selection `A.unit`, which works because the compiler created a context bound + | companion value with the (term-)name `A`. However, these context bound companions + | are not values themselves, they can only be referred to in selections. + -------------------------------------------------------------------------------------------------------------------- diff --git a/tests/neg/cb-companion-leaks.scala b/tests/neg/cb-companion-leaks.scala new file mode 100644 index 000000000000..24aa36406253 --- /dev/null +++ b/tests/neg/cb-companion-leaks.scala @@ -0,0 +1,18 @@ +//> using options -language:experimental.modularity -source future -explain + +class C: + type Self + +class D: + type Self + +trait Test: + + def foo[A: {C, D}] = A // error + + type A: C + + val x = A // error + + val y: A.type = ??? // error + diff --git a/tests/new/test.scala b/tests/new/test.scala index e6bfc29fd808..8ba66903f581 100644 --- a/tests/new/test.scala +++ b/tests/new/test.scala @@ -1,2 +1,21 @@ -object Test: - def f: Any = 1 +trait Monoid[T]{ + def unit: T + type TypeMember = Int +} +object Monoid{ + implicit object intM extends Monoid[Int]{ def unit = 0 } +} +object Main{ + opaque type ABound = Unit + def reduceSort[A](xs: List[A])(using monoid_a: Monoid[A], ordering_a: scala.math.Ordering[A]) = + inline implicit def f(v: ABound): ordering_a.type = ordering_a + inline implicit def g(v: ABound): monoid_a.type = monoid_a + val A: ABound = null.asInstanceOf[ABound] + val i: A.TypeMember = 123 // THIS WORKS + A.unit +: xs.sorted(A) + + def main(args: Array[String]): Unit = { + println("Hello World") + println(reduceSort(List(1, 2, 3, 2, 1))) + } +} \ No newline at end of file diff --git a/tests/pos-macros/i8325/Macro_1.scala b/tests/pos-macros/i8325/Macro_1.scala index 18466e17b3df..92a54d21b00a 100644 --- a/tests/pos-macros/i8325/Macro_1.scala +++ b/tests/pos-macros/i8325/Macro_1.scala @@ -3,7 +3,7 @@ package a import scala.quoted.* -object A: +object O: inline def transform[A](inline expr: A): A = ${ transformImplExpr('expr) @@ -15,7 +15,7 @@ object A: import quotes.reflect.* expr.asTerm match { case Inlined(x,y,z) => transformImplExpr(z.asExpr.asInstanceOf[Expr[A]]) - case Apply(fun,args) => '{ A.pure(${Apply(fun,args).asExpr.asInstanceOf[Expr[A]]}) } + case Apply(fun,args) => '{ O.pure(${Apply(fun,args).asExpr.asInstanceOf[Expr[A]]}) } case other => expr } } diff --git a/tests/pos-macros/i8325/Test_2.scala b/tests/pos-macros/i8325/Test_2.scala index 8b0a74b11a08..90e88dfee341 100644 --- a/tests/pos-macros/i8325/Test_2.scala +++ b/tests/pos-macros/i8325/Test_2.scala @@ -3,7 +3,7 @@ package a class Test1 { def t1(): Unit = { - A.transform( + O.transform( s"a ${1} ${2}") } diff --git a/tests/pos-macros/i8325b/Macro_1.scala b/tests/pos-macros/i8325b/Macro_1.scala index 181efa260f9b..139abed94078 100644 --- a/tests/pos-macros/i8325b/Macro_1.scala +++ b/tests/pos-macros/i8325b/Macro_1.scala @@ -3,7 +3,7 @@ package a import scala.quoted.* -object A: +object O: inline def transform[A](inline expr: A): A = ${ transformImplExpr('expr) @@ -16,7 +16,7 @@ object A: expr.asTerm match { case Inlined(x,y,z) => transformImplExpr(z.asExpr.asInstanceOf[Expr[A]]) case r@Apply(fun,args) => '{ - A.pure(${r.asExpr.asInstanceOf[Expr[A]]}) } + O.pure(${r.asExpr.asInstanceOf[Expr[A]]}) } case other => expr } } diff --git a/tests/pos-macros/i8325b/Test_2.scala b/tests/pos-macros/i8325b/Test_2.scala index 8b0a74b11a08..90e88dfee341 100644 --- a/tests/pos-macros/i8325b/Test_2.scala +++ b/tests/pos-macros/i8325b/Test_2.scala @@ -3,7 +3,7 @@ package a class Test1 { def t1(): Unit = { - A.transform( + O.transform( s"a ${1} ${2}") } diff --git a/tests/pos/FromString.scala b/tests/pos/FromString.scala index 4da8f1c9833a..892bc28d9e8d 100644 --- a/tests/pos/FromString.scala +++ b/tests/pos/FromString.scala @@ -8,5 +8,8 @@ given Int forms FromString = _.toInt given Double forms FromString = _.toDouble -def add[N: {FromString as fs, Numeric as num}](a: String, b: String): N = - num.plus(fs.fromString(a), fs.fromString(b)) +def add[N: {FromString, Numeric as num}](a: String, b: String): N = + N.plus( + num.plus(N.fromString(a), N.fromString(b)), + N.fromString(a) + ) \ No newline at end of file diff --git a/tests/pos/cb-companion-joins.scala b/tests/pos/cb-companion-joins.scala new file mode 100644 index 000000000000..e36ed2d4d864 --- /dev/null +++ b/tests/pos/cb-companion-joins.scala @@ -0,0 +1,23 @@ +import language.experimental.modularity +import language.future + +trait M: + type Self + extension (x: Self) def combine (y: Self): String + def unit: Self + +trait Num: + type Self + def zero: Self + +trait A extends M +trait B extends M + +trait AA: + type X: M +trait BB: + type X: Num +class CC[X1: {M, Num}] extends AA, BB: + type X = X1 + X.zero + X.unit diff --git a/tests/run/given-disambiguation.scala b/tests/run/given-disambiguation.scala new file mode 100644 index 000000000000..daba4d146e7f --- /dev/null +++ b/tests/run/given-disambiguation.scala @@ -0,0 +1,58 @@ +import language.experimental.modularity +import language.future + +trait M: + type Self + extension (x: Self) def combine (y: Self): String + def unit: Self + +trait Num: + type Self + def zero: Self + +trait A extends M +trait B extends M + +def f[X: {M, A, B}](x: X) = + summon[X forms M] + x.combine(x) + +trait AA: + type XX: {M, A, B} + val x = XX.unit + val A: String = "hello" + +trait AAA: + type X: M +trait BBB: + type X: Num +class CCC[X1: {M, Num}] extends AAA, BBB: + type X = X1 + X.zero + X.unit + +@main def Test = + class C + + given C forms M: + extension (x: Self) def combine (y: Self) = "M" + def unit = C() + + given C forms A: + extension (x: Self) def combine (y: Self) = "A" + def unit = C() + + given C forms B: + extension (x: Self) def combine (y: Self) = "B" + def unit = C() + + assert(f(C()) == "M") + + class CC extends AA: + type XX = C + assert(A.length == 5) + assert(A.toString == "hello") + + CC() + + From 7d754d60a5320c025a629218a401f3af7ee77073 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 9 Mar 2024 19:05:02 +0100 Subject: [PATCH 61/63] Refinements for context-bound companions Add a config setting whether or not to use the type name for unary context bounds as default name. It's on by default. If it is off, context bound companions are created instead. After fixing several problems, the test suite was verified to compile with the setting set to off. --- .../src/dotty/tools/dotc/ast/Desugar.scala | 79 ++++++++++++------- .../src/dotty/tools/dotc/config/Config.scala | 7 ++ .../src/dotty/tools/dotc/core/Contexts.scala | 13 +-- .../src/dotty/tools/dotc/core/NamerOps.scala | 50 ++++++------ .../src/dotty/tools/dotc/core/SymUtils.scala | 2 +- .../tools/dotc/core/tasty/TreeUnpickler.scala | 1 + .../tools/dotc/printing/PlainPrinter.scala | 4 +- .../src/dotty/tools/dotc/typer/Namer.scala | 63 +++++++-------- 8 files changed, 125 insertions(+), 94 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 0dc5ad721d77..a47e3f3a0b5c 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -10,7 +10,7 @@ import Annotations.Annotation import NameKinds.{UniqueName, ContextBoundParamName, ContextFunctionParamName, DefaultGetterName, WildcardParamName} import typer.{Namer, Checking} import util.{Property, SourceFile, SourcePosition, Chars} -import config.Feature +import config.{Feature, Config} import config.SourceVersion.* import collection.mutable.ListBuffer import reporting.* @@ -230,7 +230,8 @@ object desugar { tdef: TypeDef, evidenceBuf: ListBuffer[ValDef], flags: FlagSet, - freshName: => TermName)(using Context): TypeDef = + freshName: untpd.Tree => TermName, + allParamss: List[ParamClause])(using Context): TypeDef = val evidenceNames = ListBuffer[TermName]() @@ -241,11 +242,11 @@ object desugar { val evidenceName = bound match case ContextBoundTypeTree(_, _, ownName) if !ownName.isEmpty => ownName - case _ if !isMember && cxbounds.tail.isEmpty && Feature.enabled(Feature.modularity) => + case _ if !isMember && cxbounds.tail.isEmpty + && Feature.enabled(Feature.modularity) && Config.nameSingleContextBounds => tdef.name.toTermName case _ => - if isMember then inventGivenOrExtensionName(bound) - else freshName + freshName(bound) evidenceNames += evidenceName val evidenceParam = ValDef(evidenceName, bound, EmptyTree).withFlags(flags) evidenceParam.pushAttachment(ContextBoundParam, ()) @@ -257,9 +258,13 @@ object desugar { rhs val tdef1 = cpy.TypeDef(tdef)(rhs = desugarRhs(tdef.rhs)) - if evidenceNames.nonEmpty && !evidenceNames.contains(tdef.name.toTermName) then - val witnessNamesAnnot = WitnessNamesAnnot(evidenceNames.toList).withSpan(tdef.span) - tdef1.withAddedAnnotation(witnessNamesAnnot) + if Feature.enabled(Feature.modularity) + && evidenceNames.nonEmpty + && !evidenceNames.contains(tdef.name.toTermName) + && !allParamss.nestedExists(_.name == tdef.name.toTermName) + then + tdef1.withAddedAnnotation: + WitnessNamesAnnot(evidenceNames.toList).withSpan(tdef.span) else tdef1 end desugarContextBounds @@ -268,7 +273,7 @@ object desugar { val DefDef(_, paramss, tpt, rhs) = meth val evidenceParamBuf = ListBuffer[ValDef]() var seenContextBounds: Int = 0 - def freshName = + def freshName(unused: Tree) = seenContextBounds += 1 // Start at 1 like FreshNameCreator. ContextBoundParamName(EmptyTermName, seenContextBounds) // Just like with `makeSyntheticParameter` on nameless parameters of @@ -280,7 +285,7 @@ object desugar { val iflag = if Feature.sourceVersion.isAtLeast(`future`) then Given else Implicit val flags = if isPrimaryConstructor then iflag | LocalParamAccessor else iflag | Param mapParamss(paramss) { - tparam => desugarContextBounds(tparam, evidenceParamBuf, flags, freshName) + tparam => desugarContextBounds(tparam, evidenceParamBuf, flags, freshName, paramss) }(identity) rhs match @@ -326,9 +331,9 @@ object desugar { def getterParamss(n: Int): List[ParamClause] = mapParamss(takeUpTo(paramssNoRHS, n)) { - tparam => dropContextBounds(toDefParam(tparam, keepAnnotations = true)) + tparam => dropContextBounds(toDefParam(tparam, KeepAnnotations.All)) } { - vparam => toDefParam(vparam, keepAnnotations = true, keepDefault = false) + vparam => toDefParam(vparam, KeepAnnotations.All, keepDefault = false) } def defaultGetters(paramss: List[ParamClause], n: Int): List[DefDef] = paramss match @@ -433,7 +438,14 @@ object desugar { private def addEvidenceParams(meth: DefDef, params: List[ValDef])(using Context): DefDef = if params.isEmpty then return meth - val boundNames = params.map(_.name).toSet + var boundNames = params.map(_.name).toSet + for mparams <- meth.paramss; mparam <- mparams do + mparam match + case tparam: TypeDef if tparam.mods.annotations.exists(WitnessNamesAnnot.unapply(_).isDefined) => + boundNames += tparam.name.toTermName + case _ => + + //println(i"add ev params ${meth.name}, ${boundNames.toList}") def references(vdef: ValDef): Boolean = vdef.tpt.existsSubTree: @@ -464,15 +476,26 @@ object desugar { @sharable private val synthetic = Modifiers(Synthetic) - private def toDefParam(tparam: TypeDef, keepAnnotations: Boolean): TypeDef = { - var mods = tparam.rawMods - if (!keepAnnotations) mods = mods.withAnnotations(Nil) + /** Which annotations to keep in derived parameters */ + private enum KeepAnnotations: + case None, All, WitnessOnly + + /** Filter annotations in `mods` according to `keep` */ + private def filterAnnots(mods: Modifiers, keep: KeepAnnotations)(using Context) = keep match + case KeepAnnotations.None => mods.withAnnotations(Nil) + case KeepAnnotations.All => mods + case KeepAnnotations.WitnessOnly => + mods.withAnnotations: + mods.annotations.filter: + case WitnessNamesAnnot(_) => true + case _ => false + + private def toDefParam(tparam: TypeDef, keep: KeepAnnotations)(using Context): TypeDef = + val mods = filterAnnots(tparam.rawMods, keep) tparam.withMods(mods & EmptyFlags | Param) - } - private def toDefParam(vparam: ValDef, keepAnnotations: Boolean, keepDefault: Boolean)(using Context): ValDef = { - var mods = vparam.rawMods - if (!keepAnnotations) mods = mods.withAnnotations(Nil) + private def toDefParam(vparam: ValDef, keep: KeepAnnotations, keepDefault: Boolean)(using Context): ValDef = { + val mods = filterAnnots(vparam.rawMods, keep) val hasDefault = if keepDefault then HasDefault else EmptyFlags // Need to ensure that tree is duplicated since term parameters can be watched // and cloning a term parameter will copy its watchers to the clone, which means @@ -495,8 +518,10 @@ object desugar { def typeDef(tdef: TypeDef)(using Context): Tree = val evidenceBuf = new ListBuffer[ValDef] - val result = desugarContextBounds(tdef, evidenceBuf, - (tdef.mods.flags.toTermFlags & AccessFlags) | Lazy | DeferredGivenFlags, EmptyTermName) + val result = desugarContextBounds( + tdef, evidenceBuf, + (tdef.mods.flags.toTermFlags & AccessFlags) | Lazy | DeferredGivenFlags, + inventGivenOrExtensionName, Nil) if evidenceBuf.isEmpty then result else Thicket(result :: evidenceBuf.toList) /** The expansion of a class definition. See inline comments for what is involved */ @@ -571,7 +596,7 @@ object desugar { // Annotations on class _type_ parameters are set on the derived parameters // but not on the constructor parameters. The reverse is true for // annotations on class _value_ parameters. - val constrTparams = impliedTparams.map(toDefParam(_, keepAnnotations = false)) + val constrTparams = impliedTparams.map(toDefParam(_, KeepAnnotations.WitnessOnly)) def defVparamss = if (originalVparamss.isEmpty) { // ensure parameter list is non-empty if (isCaseClass) @@ -582,7 +607,7 @@ object desugar { report.error(CaseClassMissingNonImplicitParamList(cdef), namePos) ListOfNil } - else originalVparamss.nestedMap(toDefParam(_, keepAnnotations = true, keepDefault = true)) + else originalVparamss.nestedMap(toDefParam(_, KeepAnnotations.All, keepDefault = true)) val constrVparamss = defVparamss // defVparamss also needed as separate tree nodes in implicitWrappers below. // Need to be separate because they are `watch`ed in addParamRefinements. @@ -608,7 +633,7 @@ object desugar { defDef( addEvidenceParams( cpy.DefDef(ddef)(paramss = joinParams(constrTparams, ddef.paramss)), - evidenceParams(constr1).map(toDefParam(_, keepAnnotations = false, keepDefault = false))))) + evidenceParams(constr1).map(toDefParam(_, KeepAnnotations.None, keepDefault = false))))) case stat => stat } @@ -914,9 +939,9 @@ object desugar { } else { val defParamss = defVparamss.nestedMapConserve: param => - // for named context bound parameters, we assume that they might have embedded types + // for context bound parameters, we assume that they might have embedded types // so they should be treated as tracked. - if param.hasAttachment(ContextBoundParam) && !param.name.is(ContextBoundParamName) + if param.hasAttachment(ContextBoundParam) && Feature.enabled(Feature.modularity) then param.withFlags(param.mods.flags | Tracked) else param match diff --git a/compiler/src/dotty/tools/dotc/config/Config.scala b/compiler/src/dotty/tools/dotc/config/Config.scala index 2746476261e5..a7f9bdb2022d 100644 --- a/compiler/src/dotty/tools/dotc/config/Config.scala +++ b/compiler/src/dotty/tools/dotc/config/Config.scala @@ -235,4 +235,11 @@ object Config { */ inline val checkLevelsOnConstraints = false inline val checkLevelsOnInstantiation = true + + /** If a type parameter `X` has a single context bounf `X: C`, should the + * witness parameter be named `X`? This would prevent the creation of a + * context bound companion. + */ + inline val nameSingleContextBounds = true } + diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index ae21c6fb8763..97779e7c6957 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -12,6 +12,7 @@ import Symbols.* import Scopes.* import Uniques.* import ast.Trees.* +import Flags.ParamAccessor import ast.untpd import util.{NoSource, SimpleIdentityMap, SourceFile, HashSet, ReusableInstance} import typer.{Implicits, ImportInfo, SearchHistory, SearchRoot, TypeAssigner, Typer, Nullables} @@ -399,7 +400,8 @@ object Contexts { * * - as owner: The primary constructor of the class * - as outer context: The context enclosing the class context - * - as scope: The parameter accessors in the class context + * - as scope: type parameters, the parameter accessors, and + * the context bound companions in the class context, * * The reasons for this peculiar choice of attributes are as follows: * @@ -413,10 +415,11 @@ object Contexts { * context see the constructor parameters instead, but then we'd need a final substitution step * from constructor parameters to class parameter accessors. */ - def superCallContext: Context = { - val locals = newScopeWith(owner.typeParams ++ owner.asClass.paramAccessors*) - superOrThisCallContext(owner.primaryConstructor, locals) - } + def superCallContext: Context = + val locals = owner.typeParams + ++ owner.asClass.unforcedDecls.filter: sym => + sym.is(ParamAccessor) || sym.isContextBoundCompanion + superOrThisCallContext(owner.primaryConstructor, newScopeWith(locals*)) /** The context for the arguments of a this(...) constructor call. * The context is computed from the local auxiliary constructor context. diff --git a/compiler/src/dotty/tools/dotc/core/NamerOps.scala b/compiler/src/dotty/tools/dotc/core/NamerOps.scala index 347b51c01a64..863de26fbd55 100644 --- a/compiler/src/dotty/tools/dotc/core/NamerOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NamerOps.scala @@ -250,48 +250,42 @@ object NamerOps: rhsCtx.gadtState.addBound(psym, tr, isUpper = true) } - /** Create a context-bound companion for type symbol `tsym` unless it - * would clash with another parameter. `tsym` is a context-bound symbol - * that defined a set of witnesses with names `witnessNames`. + /** Create a context-bound companion for type symbol `tsym`, which has a context + * bound that defines a set of witnesses with names `witnessNames`. * - * @param paramSymss If `tsym` is a type parameter, the other parameter symbols, - * including witnesses, of the method containing `tsym`. - * If `tsym` is an abstract type member, `paramSymss` is the - * empty list. + * @param parans If `tsym` is a type parameter, a list of parameter symbols + * that include all witnesses, otherwise the empty list. * * The context-bound companion has as name the name of `tsym` translated to * a term name. We create a synthetic val of the form * - * val A: CBCompanion[witnessRef1 | ... | witnessRefN] + * val A: ``[witnessRef1 | ... | witnessRefN] * * where * - * CBCompanion is the type created in Definitions - * withnessRefK is a refence to the K'the witness. + * is the CBCompanion type created in Definitions + * withnessRefK is a refence to the K'th witness. * * The companion has the same access flags as the original type. */ - def maybeAddContextBoundCompanionFor(tsym: Symbol, witnessNames: List[TermName], paramSymss: List[List[Symbol]])(using Context): Unit = + def addContextBoundCompanionFor(tsym: Symbol, witnessNames: List[TermName], params: List[Symbol])(using Context): Unit = val prefix = ctx.owner.thisType val companionName = tsym.name.toTermName val witnessRefs = - if paramSymss.nonEmpty then - if paramSymss.nestedExists(_.name == companionName) then Nil - else - witnessNames.map: witnessName => - prefix.select(paramSymss.nestedFind(_.name == witnessName).get) + if params.nonEmpty then + witnessNames.map: witnessName => + prefix.select(params.find(_.name == witnessName).get) else - witnessNames.map(prefix.select) - if witnessRefs.nonEmpty then - val cbtype = defn.CBCompanion.typeRef.appliedTo: - witnessRefs.reduce[Type](OrType(_, _, soft = false)) - val cbc = newSymbol( - ctx.owner, companionName, - (tsym.flagsUNSAFE & AccessFlags) | Synthetic, - cbtype) - typr.println(i"contetx bpund companion created $cbc: $cbtype in ${ctx.owner}") - ctx.enter(cbc) - end maybeAddContextBoundCompanionFor + witnessNames.map(TermRef(prefix, _)) + val cbtype = defn.CBCompanion.typeRef.appliedTo: + witnessRefs.reduce[Type](OrType(_, _, soft = false)) + val cbc = newSymbol( + ctx.owner, companionName, + (tsym.flagsUNSAFE & (AccessFlags)).toTermFlags | Synthetic, + cbtype) + typr.println(s"context bound companion created $cbc for $witnessNames in ${ctx.owner}") + ctx.enter(cbc) + end addContextBoundCompanionFor /** Add context bound companions to all context-bound types declared in * this class. This assumes that these types already have their @@ -306,5 +300,5 @@ object NamerOps: if ann.symbol == defn.WitnessNamesAnnot then ann.tree match case ast.tpd.WitnessNamesAnnot(witnessNames) => - maybeAddContextBoundCompanionFor(sym, witnessNames, Nil) + addContextBoundCompanionFor(sym, witnessNames, Nil) end NamerOps diff --git a/compiler/src/dotty/tools/dotc/core/SymUtils.scala b/compiler/src/dotty/tools/dotc/core/SymUtils.scala index ec2da3cbb4ef..3a97a0053dbd 100644 --- a/compiler/src/dotty/tools/dotc/core/SymUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/SymUtils.scala @@ -88,7 +88,7 @@ class SymUtils: } def isContextBoundCompanion(using Context): Boolean = - self.is(Synthetic) && self.info.typeSymbol == defn.CBCompanion + self.is(Synthetic) && self.infoOrCompleter.typeSymbol == defn.CBCompanion /** Is this a case class for which a product mirror is generated? * Excluded are value classes, abstract classes and case classes with more than one diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index 66a761835acd..062f714261fb 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -1124,6 +1124,7 @@ class TreeUnpickler(reader: TastyReader, }) defn.patchStdLibClass(cls) NamerOps.addConstructorProxies(cls) + NamerOps.addContextBoundCompanions(cls) setSpan(start, untpd.Template(constr, mappedParents, self, lazyStats) .withType(localDummy.termRef)) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index ff6419c48801..6847b76f485e 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -430,11 +430,11 @@ class PlainPrinter(_ctx: Context) extends Printer { sym.isEffectiveRoot || sym.isAnonymousClass || sym.name.isReplWrapperName /** String representation of a definition's type following its name, - * if symbol is completed, "?" otherwise. + * if symbol is completed, ": ?" otherwise. */ protected def toTextRHS(optType: Option[Type]): Text = optType match { case Some(tp) => toTextRHS(tp) - case None => "?" + case None => ": ?" } protected def decomposeLambdas(bounds: TypeBounds): (Text, TypeBounds) = diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 0c84fa53c225..265d4c4e5830 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -406,7 +406,11 @@ class Namer { typer: Typer => enterSymbol(sym) setDocstring(sym, origStat) addEnumConstants(mdef, sym) - maybeAddCBCompanion(mdef, Nil) + mdef match + case tdef: TypeDef if ctx.owner.isClass => + for case WitnessNamesAnnot(witnessNames) <- tdef.mods.annotations do + addContextBoundCompanionFor(symbolOfTree(tdef), witnessNames, Nil) + case _ => ctx case stats: Thicket => stats.toList.foreach(recur) @@ -1663,11 +1667,9 @@ class Namer { typer: Typer => val parentTypes = defn.adjustForTuple(cls, cls.typeParams, defn.adjustForBoxedUnit(cls, - addUsingTraits( - locally: - val isJava = ctx.isJava - ensureFirstIsClass(cls, parents.map(checkedParentType(_, isJava))) - ) + addUsingTraits: + val isJava = ctx.isJava + ensureFirstIsClass(cls, parents.map(checkedParentType(_, isJava))) ) ) typr.println(i"completing $denot, parents = $parents%, %, parentTypes = $parentTypes%, %") @@ -1739,12 +1741,6 @@ class Namer { typer: Typer => val sym = tree.symbol if sym.isConstructor then sym.owner else sym - /** Enter and typecheck parameter list */ - def completeParams(params: List[MemberDef])(using Context): Unit = { - index(params) - for (param <- params) typedAheadExpr(param) - } - /** The signature of a module valdef. * This will compute the corresponding module class TypeRef immediately * without going through the defined type of the ValDef. This is necessary @@ -1843,6 +1839,30 @@ class Namer { typer: Typer => // Beware: ddef.name need not match sym.name if sym was freshened! val isConstructor = sym.name == nme.CONSTRUCTOR + val witnessNamesOfParam = mutable.Map[TypeDef, List[TermName]]() + if !ddef.name.is(DefaultGetterName) && !sym.is(Synthetic) then + for params <- ddef.paramss; case tdef: TypeDef <- params do + for case WitnessNamesAnnot(ws) <- tdef.mods.annotations do + witnessNamesOfParam(tdef) = ws + + /** Are all names in `wnames` defined by the longest prefix of all `params` + * that have been typed ahead (i.e. that carry the TypedAhead attachment)? + */ + def allParamsSeen(wnames: List[TermName], params: List[MemberDef]) = + (wnames.toSet[Name] -- params.takeWhile(_.hasAttachment(TypedAhead)).map(_.name)).isEmpty + + /** Enter and typecheck parameter list, add context companions as. + * Once all witness parameters for a context bound are seen, create a + * context bound companion for it. + */ + def completeParams(params: List[MemberDef])(using Context): Unit = + index(params) + for param <- params do + typedAheadExpr(param) + for (tdef, wnames) <- witnessNamesOfParam do + if wnames.contains(param.name) && allParamsSeen(wnames, params) then + addContextBoundCompanionFor(symbolOfTree(tdef), wnames, params.map(symbolOfTree)) + // The following 3 lines replace what was previously just completeParams(tparams). // But that can cause bad bounds being computed, as witnessed by // tests/pos/paramcycle.scala. The problematic sequence is this: @@ -1876,9 +1896,6 @@ class Namer { typer: Typer => val paramSymss = normalizeIfConstructor(ddef.paramss.nestedMap(symbolOfTree), isConstructor) sym.setParamss(paramSymss) - if !ddef.name.is(DefaultGetterName) && !sym.is(Synthetic) then - for params <- ddef.paramss; param <- params do - maybeAddCBCompanion(param, paramSymss) /** Set every context bound evidence parameter of a class to be tracked, * provided it has a type that has an abstract type member and the parameter's @@ -1894,7 +1911,6 @@ class Namer { typer: Typer => case info: TempClassInfo => if !sym.is(Tracked) && param.hasAttachment(ContextBoundParam) - && !param.name.is(ContextBoundParamName) && (sym.info.memberNames(abstractTypeNameFilter).nonEmpty || sym.maybeOwner.maybeOwner.is(Given)) then @@ -2072,21 +2088,6 @@ class Namer { typer: Typer => } end inferredResultType - /** If `mdef` is a TypeDef with a @WitnessNames annotation, add a context bound - * companion, provided `mdef` is a paremeter exactly when paramSymss is non empty. - * maybeAddCBCompanion is called in two places: - * - when analyzing a TypeDef where we want to add a companion if it is - * a member type. In this case the TypeDef is not a parameter and paramSymss is empty. - * - when analyzing a DefDef and traversing all its type parameters. - * In this case the TypeDef is a parameter and paramSymss is non-empty. - */ - def maybeAddCBCompanion(mdef: DefTree, paramSymss: List[List[Symbol]])(using Context): Unit = - mdef match - case tdef: TypeDef if mdef.mods.is(Param) == paramSymss.nonEmpty => - for case WitnessNamesAnnot(witnessNames) <- tdef.mods.annotations do - maybeAddContextBoundCompanionFor(symbolOfTree(tdef), witnessNames, paramSymss) - case _ => - /** Prepare a GADT-aware context used to type the RHS of a ValOrDefDef. */ def prepareRhsCtx(rhsCtx: FreshContext, paramss: List[List[Symbol]])(using Context): FreshContext = val typeParams = paramss.collect { case TypeSymbols(tparams) => tparams }.flatten From dc8c70836b1bd8de777315cff89c704d5a04948a Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 11 Mar 2024 22:55:43 +0100 Subject: [PATCH 62/63] Fix typo in doc page and update MimaFilters, pickling excludes --- compiler/test/dotc/pos-test-pickling.blacklist | 1 + docs/_docs/reference/experimental/typeclasses-syntax.md | 2 +- project/MiMaFilters.scala | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/test/dotc/pos-test-pickling.blacklist b/compiler/test/dotc/pos-test-pickling.blacklist index f6f8a2514e5e..80f5846a4798 100644 --- a/compiler/test/dotc/pos-test-pickling.blacklist +++ b/compiler/test/dotc/pos-test-pickling.blacklist @@ -120,6 +120,7 @@ i15525.scala # alias types at different levels of dereferencing parsercombinators-givens.scala +parsercombinators-givens-2.scala parsercombinators-ctx-bounds.scala parsercombinators-this.scala parsercombinators-arrow.scala diff --git a/docs/_docs/reference/experimental/typeclasses-syntax.md b/docs/_docs/reference/experimental/typeclasses-syntax.md index 1d746a995dad..1264bcec269d 100644 --- a/docs/_docs/reference/experimental/typeclasses-syntax.md +++ b/docs/_docs/reference/experimental/typeclasses-syntax.md @@ -34,7 +34,7 @@ def reduce[A : Monoid](xs: List[A]): A = ??? Since we don't have a name for the `Monoid` instance of `A`, we need to resort to `summon` in the body of `reduce`: ```scala def reduce[A : Monoid](xs: List[A]): A = - xs.foldLeft(summon Monoid[A])(_ `combine` _) + xs.foldLeft(summon[Monoid[A]].unit)(_ `combine` _) ``` That's generally considered too painful to write and read, hence people usually adopt one of two alternatives. Either, eschew context bounds and switch to using clauses: ```scala diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index aad09834dcd1..22591104ea41 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -11,6 +11,7 @@ object MiMaFilters { ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.modularity"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$modularity$"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.compiletime.package#package.deferred"), + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.WitnessNames"), ), // Additions since last LTS From 7c8116c16ebf67ae44d53f4d47cd4578df8ce14e Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 1 Apr 2024 10:35:49 +0200 Subject: [PATCH 63/63] More regular handling of tracked for given companion methods Align handling of tracked with constructors. This means - Do it in Namer instead of Desugar - Only add tracked to context bound witnesses if they have abstract type members. --- .../src/dotty/tools/dotc/ast/Desugar.scala | 35 ++++------------ .../src/dotty/tools/dotc/core/NamerOps.scala | 16 ++++++-- .../tools/dotc/core/tasty/TreeUnpickler.scala | 5 ++- .../src/dotty/tools/dotc/typer/Namer.scala | 41 +++++++++++-------- 4 files changed, 48 insertions(+), 49 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index a47e3f3a0b5c..6a988f4e375f 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -597,7 +597,7 @@ object desugar { // but not on the constructor parameters. The reverse is true for // annotations on class _value_ parameters. val constrTparams = impliedTparams.map(toDefParam(_, KeepAnnotations.WitnessOnly)) - def defVparamss = + val constrVparamss = if (originalVparamss.isEmpty) { // ensure parameter list is non-empty if (isCaseClass) report.error(CaseClassMissingParamList(cdef), namePos) @@ -608,10 +608,6 @@ object desugar { ListOfNil } else originalVparamss.nestedMap(toDefParam(_, KeepAnnotations.All, keepDefault = true)) - val constrVparamss = defVparamss - // defVparamss also needed as separate tree nodes in implicitWrappers below. - // Need to be separate because they are `watch`ed in addParamRefinements. - // See parsercombinators-givens.scala for a test case. val derivedTparams = constrTparams.zipWithConserve(impliedTparams)((tparam, impliedParam) => derivedTypeParam(tparam).withAnnotations(impliedParam.mods.annotations)) @@ -710,14 +706,6 @@ object desugar { appliedTypeTree(tycon, targs) } - def addParamRefinements(core: Tree, paramss: List[List[ValDef]]): Tree = - val refinements = - for params <- paramss; param <- params; if param.mods.is(Tracked) yield - ValDef(param.name, SingletonTypeTree(TermRefTree().watching(param)), EmptyTree) - .withSpan(param.span) - if refinements.isEmpty then core - else RefinedTypeTree(core, refinements).showing(i"refined result: $result", Printers.desugar) - // a reference to the class type bound by `cdef`, with type parameters coming from the constructor val classTypeRef = appliedRef(classTycon) @@ -938,24 +926,17 @@ object desugar { Nil } else { - val defParamss = defVparamss.nestedMapConserve: param => - // for context bound parameters, we assume that they might have embedded types - // so they should be treated as tracked. - if param.hasAttachment(ContextBoundParam) && Feature.enabled(Feature.modularity) - then param.withFlags(param.mods.flags | Tracked) - else param - match - case Nil :: paramss => - paramss // drop leading () that got inserted by class - // TODO: drop this once we do not silently insert empty class parameters anymore - case paramss => paramss + val defParamss = constrVparamss match + case Nil :: paramss => + paramss // drop leading () that got inserted by class + // TODO: drop this once we do not silently insert empty class parameters anymore + case paramss => paramss val finalFlag = if ctx.settings.YcompileScala2Library.value then EmptyFlags else Final // implicit wrapper is typechecked in same scope as constructor, so // we can reuse the constructor parameters; no derived params are needed. DefDef( - className.toTermName, joinParams(constrTparams, defParamss), - addParamRefinements(classTypeRef, defParamss), creatorExpr) - .withMods(companionMods | mods.flags.toTermFlags & (GivenOrImplicit | Inline) | finalFlag) + className.toTermName, joinParams(constrTparams, defParamss), classTypeRef, creatorExpr + ) .withMods(companionMods | mods.flags.toTermFlags & (GivenOrImplicit | Inline) | finalFlag) .withSpan(cdef.span) :: Nil } diff --git a/compiler/src/dotty/tools/dotc/core/NamerOps.scala b/compiler/src/dotty/tools/dotc/core/NamerOps.scala index 863de26fbd55..58b4ad681c6f 100644 --- a/compiler/src/dotty/tools/dotc/core/NamerOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NamerOps.scala @@ -17,12 +17,20 @@ object NamerOps: * @param ctor the constructor */ def effectiveResultType(ctor: Symbol, paramss: List[List[Symbol]])(using Context): Type = - val (resType, termParamss) = paramss match + paramss match case TypeSymbols(tparams) :: rest => - (ctor.owner.typeRef.appliedTo(tparams.map(_.typeRef)), rest) + addParamRefinements(ctor.owner.typeRef.appliedTo(tparams.map(_.typeRef)), rest) case _ => - (ctor.owner.typeRef, paramss) - termParamss.flatten.foldLeft(resType): (rt, param) => + addParamRefinements(ctor.owner.typeRef, paramss) + + /** Given a method with tracked term-parameters `p1, ..., pn`, and result type `R`, add the + * refinements R { p1 = p1' } ... { pn = pn' }, where pi' is the term parameter ref + * of the parameter and pi is its name. This matters only under experimental.modularity, + * since wothout it there are no tracked parameters. Parameter refinements are added for + * constructors and given companion methods. + */ + def addParamRefinements(resType: Type, paramss: List[List[Symbol]])(using Context): Type = + paramss.flatten.foldLeft(resType): (rt, param) => if param.is(Tracked) then RefinedType(rt, param.name, param.termRef) else rt diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index 062f714261fb..3f8d78663762 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -31,7 +31,8 @@ import util.{SourceFile, Property} import ast.{Trees, tpd, untpd} import Trees.* import Decorators.* -import dotty.tools.dotc.quoted.QuotePatterns +import config.Feature +import quoted.QuotePatterns import dotty.tools.tasty.{TastyBuffer, TastyReader} import TastyBuffer.* @@ -919,6 +920,8 @@ class TreeUnpickler(reader: TastyReader, val resType = if name == nme.CONSTRUCTOR then effectiveResultType(sym, paramss) + else if sym.isAllOf(Given | Method) && Feature.enabled(Feature.modularity) then + addParamRefinements(tpt.tpe, paramss) else tpt.tpe sym.info = methodType(paramss, resType) diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 265d4c4e5830..e101ff906522 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -1896,41 +1896,48 @@ class Namer { typer: Typer => val paramSymss = normalizeIfConstructor(ddef.paramss.nestedMap(symbolOfTree), isConstructor) sym.setParamss(paramSymss) + /** We add `tracked` to context bound witnesses that have abstract type members */ + def needsTracked(sym: Symbol, param: ValDef)(using Context) = + !sym.is(Tracked) + && param.hasAttachment(ContextBoundParam) + && sym.info.memberNames(abstractTypeNameFilter).nonEmpty /** Set every context bound evidence parameter of a class to be tracked, - * provided it has a type that has an abstract type member and the parameter's - * name is not a synthetic, unaddressable name. Reset private and local flags - * so that the parameter becomes a `val`. Do the same for all context bound - * evidence parameters of a `given` class. This is because in Desugar.addParamRefinements - * we create refinements for these parameters in the result type of the implicit - * access method. + * provided it has a type that has an abstract type member. Reset private and local flags + * so that the parameter becomes a `val`. */ def setTracked(param: ValDef): Unit = val sym = symbolOfTree(param) sym.maybeOwner.maybeOwner.infoOrCompleter match - case info: TempClassInfo => - if !sym.is(Tracked) - && param.hasAttachment(ContextBoundParam) - && (sym.info.memberNames(abstractTypeNameFilter).nonEmpty - || sym.maybeOwner.maybeOwner.is(Given)) - then - typr.println(i"set tracked $param, $sym: ${sym.info} containing ${sym.info.memberNames(abstractTypeNameFilter).toList}") - for acc <- info.decls.lookupAll(sym.name) if acc.is(ParamAccessor) do - acc.resetFlag(PrivateLocal) - acc.setFlag(Tracked) - sym.setFlag(Tracked) + case info: TempClassInfo if needsTracked(sym, param) => + typr.println(i"set tracked $param, $sym: ${sym.info} containing ${sym.info.memberNames(abstractTypeNameFilter).toList}") + for acc <- info.decls.lookupAll(sym.name) if acc.is(ParamAccessor) do + acc.resetFlag(PrivateLocal) + acc.setFlag(Tracked) + sym.setFlag(Tracked) case _ => def wrapMethType(restpe: Type): Type = instantiateDependent(restpe, paramSymss) methodType(paramSymss, restpe, ddef.mods.is(JavaDefined)) + def wrapRefinedMethType(restpe: Type): Type = + wrapMethType(addParamRefinements(restpe, paramSymss)) + if isConstructor then if sym.isPrimaryConstructor && Feature.enabled(modularity) then ddef.termParamss.foreach(_.foreach(setTracked)) // set result type tree to unit, but take the current class as result type of the symbol typedAheadType(ddef.tpt, defn.UnitType) wrapMethType(effectiveResultType(sym, paramSymss)) + else if sym.isAllOf(Given | Method) && Feature.enabled(modularity) then + // set every context bound evidence parameter of a given companion method + // to be tracked, provided it has a type that has an abstract type member. + // Add refinements for all tracked parameters to the result type. + for params <- ddef.termParamss; param <- params do + val psym = symbolOfTree(param) + if needsTracked(psym, param) then psym.setFlag(Tracked) + valOrDefDefSig(ddef, sym, paramSymss, wrapRefinedMethType) else valOrDefDefSig(ddef, sym, paramSymss, wrapMethType) end defDefSig