From 6ad4d8ccfbd9952094207c384bbc714cfc1f8f86 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 22 Nov 2024 17:03:34 +0100 Subject: [PATCH 1/3] Expand value references to packages to their underlying package objects A package object can be seen as the facade of a package. For instance, it is the logical place where we want to write doc comments that explain a package. So far references to packages cannot be used as values. But if the package has a package object, it would make sense to allow the package reference with the meaning that it refers to this object. For instance, let's say we have ```scala package a object b ``` Of course, we can use `a.b` as a value. But if we change that to ```scala package a package object b ``` we can't anymore. This PR changes that so that we still allow a reference `a.b` as a value to mean the package object. Due to the way package objects are encoded the `a.b` reference expands to `a.b.package`. --- .../src/dotty/tools/dotc/config/Feature.scala | 1 + .../tools/dotc/reporting/ErrorMessageID.scala | 2 +- .../dotty/tools/dotc/reporting/messages.scala | 12 ++--- .../dotty/tools/dotc/transform/Erasure.scala | 6 +-- .../src/dotty/tools/dotc/typer/Checking.scala | 18 ------- .../src/dotty/tools/dotc/typer/Typer.scala | 51 +++++++++++++------ .../runtime/stdLibPatches/language.scala | 5 ++ tests/neg/i21696.check | 6 +-- tests/run/pkgobjvals.check | 4 ++ tests/run/pkgobjvals.scala | 22 ++++++++ 10 files changed, 81 insertions(+), 46 deletions(-) create mode 100644 tests/run/pkgobjvals.check create mode 100644 tests/run/pkgobjvals.scala diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index ad20bab46c1e..a2a7fce4f8dc 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -38,6 +38,7 @@ object Feature: val betterMatchTypeExtractors = experimental("betterMatchTypeExtractors") val quotedPatternsWithPolymorphicFunctions = experimental("quotedPatternsWithPolymorphicFunctions") val betterFors = experimental("betterFors") + val packageObjectValues = experimental("packageObjectValues") def experimentalAutoEnableFeatures(using Context): List[TermName] = defn.languageExperimentalFeatures diff --git a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala index 35c170858bbf..6257eb0b1f64 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -132,7 +132,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe case MissingCompanionForStaticID // errorNumber: 116 case PolymorphicMethodMissingTypeInParentID // errorNumber: 117 case ParamsNoInlineID // errorNumber: 118 - case JavaSymbolIsNotAValueID // errorNumber: 119 + case SymbolIsNotAValueID // errorNumber: 119 case DoubleDefinitionID // errorNumber: 120 case MatchCaseOnlyNullWarningID // errorNumber: 121 case ImportedTwiceID // errorNumber: 122 diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index f94a4b58d6fb..efb7354a4bcb 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -2317,7 +2317,7 @@ class ParamsNoInline(owner: Symbol)(using Context) def explain(using Context) = "" } -class JavaSymbolIsNotAValue(symbol: Symbol)(using Context) extends TypeMsg(JavaSymbolIsNotAValueID) { +class SymbolIsNotAValue(symbol: Symbol)(using Context) extends TypeMsg(SymbolIsNotAValueID) { def msg(using Context) = val kind = if symbol is Package then i"$symbol" @@ -3331,14 +3331,14 @@ final class QuotedTypeMissing(tpe: Type)(using Context) extends StagingMessage(Q private def witness = defn.QuotedTypeClass.typeRef.appliedTo(tpe) - override protected def msg(using Context): String = + override protected def msg(using Context): String = i"Reference to $tpe within quotes requires a given ${witness} in scope" override protected def explain(using Context): String = - i"""Referencing `$tpe` inside a quoted expression requires a `${witness}` to be in scope. + i"""Referencing `$tpe` inside a quoted expression requires a `${witness}` to be in scope. |Since Scala is subject to erasure at runtime, the type information will be missing during the execution of the code. - |`${witness}` is therefore needed to carry `$tpe`'s type information into the quoted code. - |Without an implicit `${witness}`, the type `$tpe` cannot be properly referenced within the expression. + |`${witness}` is therefore needed to carry `$tpe`'s type information into the quoted code. + |Without an implicit `${witness}`, the type `$tpe` cannot be properly referenced within the expression. |To resolve this, ensure that a `${witness}` is available, either through a context-bound or explicitly. |""" @@ -3350,7 +3350,7 @@ final class AmbiguousNamedTupleAssignment(key: Name, value: untpd.Tree)(using Co |not as an assignment. | |To assign a value, use curly braces: `{${key} = ${value}}`.""" - + override protected def explain(using Context): String = "" class AmbiguousNamedTupleInfixApply()(using Context) extends SyntaxMsg(AmbiguousNamedTupleInfixApplyID): diff --git a/compiler/src/dotty/tools/dotc/transform/Erasure.scala b/compiler/src/dotty/tools/dotc/transform/Erasure.scala index 7414ca7e69c6..25239aee59cf 100644 --- a/compiler/src/dotty/tools/dotc/transform/Erasure.scala +++ b/compiler/src/dotty/tools/dotc/transform/Erasure.scala @@ -24,7 +24,6 @@ import typer.NoChecking import inlines.Inlines import typer.ProtoTypes.* import typer.ErrorReporting.errorTree -import typer.Checking.checkValue import core.TypeErasure.* import core.Decorators.* import dotty.tools.dotc.ast.{tpd, untpd} @@ -676,7 +675,7 @@ object Erasure { if tree.name == nme.apply && integrateSelect(tree) then return typed(tree.qualifier, pt) - val qual1 = typed(tree.qualifier, AnySelectionProto) + var qual1 = typed(tree.qualifier, AnySelectionProto) def mapOwner(sym: Symbol): Symbol = if !sym.exists && tree.name == nme.apply then @@ -725,7 +724,8 @@ object Erasure { assert(sym.exists, i"no owner from $owner/${origSym.showLocated} in $tree") - if owner == defn.ObjectClass then checkValue(qual1) + if owner == defn.ObjectClass then + qual1 = checkValue(qual1) def select(qual: Tree, sym: Symbol): Tree = untpd.cpy.Select(tree)(qual, sym.name).withType(NamedType(qual.tpe, sym)) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 1cd531046753..f4cffee081ae 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -781,24 +781,6 @@ object Checking { else "Cannot override non-inline parameter with an inline parameter", p1.srcPos) - def checkValue(tree: Tree)(using Context): Unit = - val sym = tree.tpe.termSymbol - if sym.isNoValue && !ctx.isJava then - report.error(JavaSymbolIsNotAValue(sym), tree.srcPos) - - /** Check that `tree` refers to a value, unless `tree` is selected or applied - * (singleton types x.type don't count as selections). - */ - def checkValue(tree: Tree, proto: Type)(using Context): tree.type = - tree match - case tree: RefTree if tree.name.isTermName => - proto match - case _: SelectionProto if proto ne SingletonTypeProto => // no value check - case _: FunOrPolyProto => // no value check - case _ => checkValue(tree) - case _ => - tree - /** Check that experimental language imports in `trees` * are done only in experimental scopes. For top-level * experimental imports, all top-level definitions are transformed diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 13f7b4eb1726..3b457fd117fc 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -609,10 +609,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // Shortcut for the root package, this is not just a performance // optimization, it also avoids forcing imports thus potentially avoiding // cyclic references. - if (name == nme.ROOTPKG) - val tree2 = tree.withType(defn.RootPackage.termRef) - checkLegalValue(tree2, pt) - return tree2 + if name == nme.ROOTPKG then + return checkLegalValue(tree.withType(defn.RootPackage.termRef), pt) val rawType = val saved1 = unimported @@ -672,9 +670,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer cpy.Ident(tree)(tree.name.unmangleClassName).withType(checkedType) else tree.withType(checkedType) - val tree2 = toNotNullTermRef(tree1, pt) - checkLegalValue(tree2, pt) - tree2 + checkLegalValue(toNotNullTermRef(tree1, pt), pt) def isLocalExtensionMethodRef: Boolean = rawType match case rawType: TermRef => @@ -714,21 +710,47 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer errorTree(tree, MissingIdent(tree, kind, name, pt)) end typedIdent + def checkValue(tree: Tree)(using Context): Tree = + val sym = tree.tpe.termSymbol + if sym.isNoValue && !ctx.isJava then + if sym.is(Package) + && Feature.enabled(Feature.packageObjectValues) + && tree.tpe.member(nme.PACKAGE).hasAltWith(_.symbol.isPackageObject) + then + typed(untpd.Select(untpd.TypedSplice(tree), nme.PACKAGE)) + else + report.error(SymbolIsNotAValue(sym), tree.srcPos) + tree + else tree + + /** Check that `tree` refers to a value, unless `tree` is selected or applied + * (singleton types x.type don't count as selections). + */ + def checkValue(tree: Tree, proto: Type)(using Context): Tree = + tree match + case tree: RefTree if tree.name.isTermName => + proto match + case _: SelectionProto if proto ne SingletonTypeProto => tree // no value check + case _: FunOrPolyProto => tree // no value check + case _ => checkValue(tree) + case _ => tree + /** (1) If this reference is neither applied nor selected, check that it does * not refer to a package or Java companion object. * (2) Check that a stable identifier pattern is indeed stable (SLS 8.1.5) */ - private def checkLegalValue(tree: Tree, pt: Type)(using Context): Unit = - checkValue(tree, pt) + private def checkLegalValue(tree: Tree, pt: Type)(using Context): Tree = + val tree1 = checkValue(tree, pt) if ctx.mode.is(Mode.Pattern) - && !tree.isType + && !tree1.isType && !pt.isInstanceOf[ApplyingProto] - && !tree.tpe.match + && !tree1.tpe.match case tp: NamedType => tp.denot.hasAltWith(_.symbol.isStableMember && tp.prefix.isStable || tp.info.isStable) case tp => tp.isStable - && !isWildcardArg(tree) + && !isWildcardArg(tree1) then - report.error(StableIdentPattern(tree, pt), tree.srcPos) + report.error(StableIdentPattern(tree1, pt), tree1.srcPos) + tree1 def typedSelectWithAdapt(tree0: untpd.Select, pt: Type, qual: Tree)(using Context): Tree = val selName = tree0.name @@ -742,8 +764,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer if checkedType.exists then val select = toNotNullTermRef(assignType(tree, checkedType), pt) if selName.isTypeName then checkStable(qual.tpe, qual.srcPos, "type prefix") - checkLegalValue(select, pt) - ConstFold(select) + ConstFold(checkLegalValue(select, pt)) else EmptyTree // Otherwise, simplify `m.apply(...)` to `m(...)` diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index b8d990cf56f5..1afdbdd3e79d 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -141,6 +141,11 @@ object language: */ @compileTimeOnly("`betterFors` can only be used at compile time in import statements") object betterFors + + /** Experimental support for package object values + */ + @compileTimeOnly("`packageObjectValues` can only be used at compile time in import statements") + object packageObjectValues end experimental /** The deprecated object contains features that are no longer officially suypported in Scala. diff --git a/tests/neg/i21696.check b/tests/neg/i21696.check index 9195263040b3..ce4844782107 100644 --- a/tests/neg/i21696.check +++ b/tests/neg/i21696.check @@ -5,9 +5,9 @@ |--------------------------------------------------------------------------------------------------------------------- | Explanation (enabled by `-explain`) |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | Referencing `T` inside a quoted expression requires a `scala.quoted.Type[T]` to be in scope. + | Referencing `T` inside a quoted expression requires a `scala.quoted.Type[T]` to be in scope. | Since Scala is subject to erasure at runtime, the type information will be missing during the execution of the code. - | `scala.quoted.Type[T]` is therefore needed to carry `T`'s type information into the quoted code. - | Without an implicit `scala.quoted.Type[T]`, the type `T` cannot be properly referenced within the expression. + | `scala.quoted.Type[T]` is therefore needed to carry `T`'s type information into the quoted code. + | Without an implicit `scala.quoted.Type[T]`, the type `T` cannot be properly referenced within the expression. | To resolve this, ensure that a `scala.quoted.Type[T]` is available, either through a context-bound or explicitly. --------------------------------------------------------------------------------------------------------------------- diff --git a/tests/run/pkgobjvals.check b/tests/run/pkgobjvals.check new file mode 100644 index 000000000000..3e327fcc0c3e --- /dev/null +++ b/tests/run/pkgobjvals.check @@ -0,0 +1,4 @@ +Foo was created +Foo was created +Foo was created +Foo was created diff --git a/tests/run/pkgobjvals.scala b/tests/run/pkgobjvals.scala new file mode 100644 index 000000000000..8df1a984642c --- /dev/null +++ b/tests/run/pkgobjvals.scala @@ -0,0 +1,22 @@ +import language.experimental.packageObjectValues + +package a: + package object b: + class Foo: + println("Foo was created") + + def foo() = Foo() + end b + + def test = + val bb = b + bb.foo() + new bb.Foo() +end a + +@main def Test = + a.test + val ab: a.b.type = a.b + ab.foo() + new ab.Foo() + From f6c6dfa97cf04b2531fb60a106a61015b641c7a9 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 25 Nov 2024 18:54:10 +0100 Subject: [PATCH 2/3] Add Mimafilters --- project/MiMaFilters.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 00e7153bcb83..e24db7c2988e 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -13,6 +13,8 @@ object MiMaFilters { ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.quotedPatternsWithPolymorphicFunctions"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$quotedPatternsWithPolymorphicFunctions$"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.runtime.Patterns.higherOrderHoleWithTypes"), + ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.packageObjectValues"), + ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$packageObjectValues$"), ), // Additions since last LTS From 944425d1c18593626952e52615770f9daf1b091a Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 25 Nov 2024 19:19:20 +0100 Subject: [PATCH 3/3] Change doc pages - Instead of "Dropped: package objects" have a new doc page "Toplevel Definitions" in "other new features". - Add a doc page for experimental reference-able package object, which uses some wording from the Pre-SIP. --- .../experimental/package-object-values.md | 40 ++++++++++++++++++ .../toplevel-definitions.md | 41 +++++++++++++++++++ docs/sidebar.yml | 3 +- 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 docs/_docs/reference/experimental/package-object-values.md create mode 100644 docs/_docs/reference/other-new-features/toplevel-definitions.md diff --git a/docs/_docs/reference/experimental/package-object-values.md b/docs/_docs/reference/experimental/package-object-values.md new file mode 100644 index 000000000000..1ca9c701970a --- /dev/null +++ b/docs/_docs/reference/experimental/package-object-values.md @@ -0,0 +1,40 @@ +--- +layout: doc-page +title: "Reference-able Package Objects" +redirectFrom: /docs/reference/experimental/package-object-values.html +nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/package-object-values.html +--- + +One limitation with `package object`s is that we cannot currently assign them to values: `a.b` fails to compile when `b` is a `package object`, even though it succeeds when `b` is a normal `object`. The workaround is to call +```scala + a.b.`package` +``` +But this is ugly and non-obvious. Or one could use a normal `object`, which is not always possible. + +The `packageObjectValues` language extension drops this limitation. The extension is enabled by the language import `import scala.language.experimental.packageObjectValues` or by setting the command line option `-language:experimental.packageObjectValues`. + +The extension, turns the following into valid code: + +```scala +package a +package object b + +val z = a.b // Currently fails with "package is not a value" +``` + +Currently the workaround is to use a `.package` suffix: + +```scala +val z = a.b.`package` +``` + +With the extension, a reference such as `a.b` where `b` is a `package` containing a `package object`, expands to `a.b.package` automatically + +## Limitations + +* `a.b` only expands to `a.b.package` when used "standalone", i.e. not when part of a larger select chain `a.b.c` or equivalent postfix expression `a.b c`, prefix expression `!a.b`, or infix expression `a.b c d`. + +* `a.b` expands to `a.b.package` of the type `a.b.package.type`, and only contains the contents of the `package object`. It does not contain other things in the `package` `a.b` that are outside of the `package object` + +Both these requirements are necessary for backwards compatibility, and anyway do not impact the main goal of removing the irregularity between `package object`s and normal `object`s. + diff --git a/docs/_docs/reference/other-new-features/toplevel-definitions.md b/docs/_docs/reference/other-new-features/toplevel-definitions.md new file mode 100644 index 000000000000..b1793bd1941c --- /dev/null +++ b/docs/_docs/reference/other-new-features/toplevel-definitions.md @@ -0,0 +1,41 @@ +--- +layout: doc-page +title: "Toplevel Definitions" +nightlyOf: https://docs.scala-lang.org/scala3/reference/dropped-features/toplevel-definitions.html +--- + +All kind of definitions can now be written at the top-level. +Example: +```scala +package p +type Labelled[T] = (String, T) +val a: Labelled[Int] = ("count", 1) +def b = a._2 + +case class C() + +extension (x: C) def pair(y: C) = (x, y) +``` +Previously, `type`, `val` or `def` definitions had to be wrapped in a package object. Now, +there may be several source files in a package containing such top-level definitions, and source files can freely mix top-level value, method, and type definitions with classes and objects. + +The compiler generates synthetic objects that wrap top-level definitions falling into one of the following categories: + + - all pattern, value, method, and type definitions, + - implicit classes and objects, + - companion objects of opaque type aliases. + +If a source file `src.scala` contains such top-level definitions, they will be put in a synthetic object named `src$package`. The wrapping is transparent, however. The definitions in `src` can still be accessed as members of the enclosing package. The synthetic object will be placed last in the file, +after any other package clauses, imports, or object and class definitions. + +**Note:** This means that +1. The name of a source file containing wrapped top-level definitions is relevant for binary compatibility. If the name changes, so does the name of the generated object and its class. + +2. A top-level main method `def main(args: Array[String]): Unit = ...` is wrapped as any other method. If it appears +in a source file `src.scala`, it could be invoked from the command line using a command like `scala src$package`. Since the +"program name" is mangled it is recommended to always put `main` methods in explicitly named objects. + +3. The notion of `private` is independent of whether a definition is wrapped or not. A `private` top-level definition is always visible from everywhere in the enclosing package. + +4. If several top-level definitions are overloaded variants with the same name, +they must all come from the same source file. diff --git a/docs/sidebar.yml b/docs/sidebar.yml index 74aee3dfc668..38a949b02584 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -72,6 +72,7 @@ subsection: - page: reference/other-new-features/export.md - page: reference/other-new-features/opaques.md - page: reference/other-new-features/opaques-details.md + - page: reference/other-new-features/toplevel-definitions.md - page: reference/other-new-features/named-tuples.md - page: reference/other-new-features/open-classes.md - page: reference/other-new-features/parameter-untupling.md @@ -124,7 +125,6 @@ subsection: - page: reference/dropped-features/type-projection.md - page: reference/dropped-features/do-while.md - page: reference/dropped-features/procedure-syntax.md - - page: reference/dropped-features/package-objects.md - page: reference/dropped-features/early-initializers.md - page: reference/dropped-features/class-shadowing.md - page: reference/dropped-features/class-shadowing-spec.md @@ -163,6 +163,7 @@ subsection: - page: reference/experimental/typeclasses.md - page: reference/experimental/runtimeChecked.md - page: reference/experimental/better-fors.md + - page: reference/experimental/package-object-values.md - page: reference/syntax.md - title: Language Versions index: reference/language-versions/language-versions.md