Skip to content

Commit fb66f34

Browse files
authored
Expand value references to packages to their underlying package objects (#22011)
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`.
2 parents 93ffd23 + 6e07688 commit fb66f34

File tree

13 files changed

+157
-39
lines changed

13 files changed

+157
-39
lines changed

compiler/src/dotty/tools/dotc/config/Feature.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ object Feature:
3737
val betterMatchTypeExtractors = experimental("betterMatchTypeExtractors")
3838
val quotedPatternsWithPolymorphicFunctions = experimental("quotedPatternsWithPolymorphicFunctions")
3939
val betterFors = experimental("betterFors")
40+
val packageObjectValues = experimental("packageObjectValues")
4041

4142
def experimentalAutoEnableFeatures(using Context): List[TermName] =
4243
defn.languageExperimentalFeatures

compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe
132132
case MissingCompanionForStaticID // errorNumber: 116
133133
case PolymorphicMethodMissingTypeInParentID // errorNumber: 117
134134
case ParamsNoInlineID // errorNumber: 118
135-
case JavaSymbolIsNotAValueID // errorNumber: 119
135+
case SymbolIsNotAValueID // errorNumber: 119
136136
case DoubleDefinitionID // errorNumber: 120
137137
case MatchCaseOnlyNullWarningID // errorNumber: 121
138138
case ImportedTwiceID // errorNumber: 122

compiler/src/dotty/tools/dotc/reporting/messages.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2317,7 +2317,7 @@ class ParamsNoInline(owner: Symbol)(using Context)
23172317
def explain(using Context) = ""
23182318
}
23192319

2320-
class JavaSymbolIsNotAValue(symbol: Symbol)(using Context) extends TypeMsg(JavaSymbolIsNotAValueID) {
2320+
class SymbolIsNotAValue(symbol: Symbol)(using Context) extends TypeMsg(SymbolIsNotAValueID) {
23212321
def msg(using Context) =
23222322
val kind =
23232323
if symbol is Package then i"$symbol"

compiler/src/dotty/tools/dotc/transform/Erasure.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import typer.NoChecking
2424
import inlines.Inlines
2525
import typer.ProtoTypes.*
2626
import typer.ErrorReporting.errorTree
27-
import typer.Checking.checkValue
2827
import core.TypeErasure.*
2928
import core.Decorators.*
3029
import dotty.tools.dotc.ast.{tpd, untpd}
@@ -676,7 +675,7 @@ object Erasure {
676675
if tree.name == nme.apply && integrateSelect(tree) then
677676
return typed(tree.qualifier, pt)
678677

679-
val qual1 = typed(tree.qualifier, AnySelectionProto)
678+
var qual1 = typed(tree.qualifier, AnySelectionProto)
680679

681680
def mapOwner(sym: Symbol): Symbol =
682681
if !sym.exists && tree.name == nme.apply then
@@ -725,7 +724,8 @@ object Erasure {
725724

726725
assert(sym.exists, i"no owner from $owner/${origSym.showLocated} in $tree")
727726

728-
if owner == defn.ObjectClass then checkValue(qual1)
727+
if owner == defn.ObjectClass then
728+
qual1 = checkValue(qual1)
729729

730730
def select(qual: Tree, sym: Symbol): Tree =
731731
untpd.cpy.Select(tree)(qual, sym.name).withType(NamedType(qual.tpe, sym))

compiler/src/dotty/tools/dotc/typer/Checking.scala

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -804,24 +804,6 @@ object Checking {
804804
else "Cannot override non-inline parameter with an inline parameter",
805805
p1.srcPos)
806806

807-
def checkValue(tree: Tree)(using Context): Unit =
808-
val sym = tree.tpe.termSymbol
809-
if sym.isNoValue && !ctx.isJava then
810-
report.error(JavaSymbolIsNotAValue(sym), tree.srcPos)
811-
812-
/** Check that `tree` refers to a value, unless `tree` is selected or applied
813-
* (singleton types x.type don't count as selections).
814-
*/
815-
def checkValue(tree: Tree, proto: Type)(using Context): tree.type =
816-
tree match
817-
case tree: RefTree if tree.name.isTermName =>
818-
proto match
819-
case _: SelectionProto if proto ne SingletonTypeProto => // no value check
820-
case _: FunOrPolyProto => // no value check
821-
case _ => checkValue(tree)
822-
case _ =>
823-
tree
824-
825807
/** Check that experimental language imports in `trees`
826808
* are done only in experimental scopes. For top-level
827809
* experimental imports, all top-level definitions are transformed

compiler/src/dotty/tools/dotc/typer/Typer.scala

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -618,10 +618,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
618618
// Shortcut for the root package, this is not just a performance
619619
// optimization, it also avoids forcing imports thus potentially avoiding
620620
// cyclic references.
621-
if (name == nme.ROOTPKG)
622-
val tree2 = tree.withType(defn.RootPackage.termRef)
623-
checkLegalValue(tree2, pt)
624-
return tree2
621+
if name == nme.ROOTPKG then
622+
return checkLegalValue(tree.withType(defn.RootPackage.termRef), pt)
625623

626624
val rawType =
627625
val saved1 = unimported
@@ -681,9 +679,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
681679
cpy.Ident(tree)(tree.name.unmangleClassName).withType(checkedType)
682680
else
683681
tree.withType(checkedType)
684-
val tree2 = toNotNullTermRef(tree1, pt)
685-
checkLegalValue(tree2, pt)
686-
tree2
682+
checkLegalValue(toNotNullTermRef(tree1, pt), pt)
687683

688684
def isLocalExtensionMethodRef: Boolean = rawType match
689685
case rawType: TermRef =>
@@ -723,21 +719,47 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
723719
errorTree(tree, MissingIdent(tree, kind, name, pt))
724720
end typedIdent
725721

722+
def checkValue(tree: Tree)(using Context): Tree =
723+
val sym = tree.tpe.termSymbol
724+
if sym.isNoValue && !ctx.isJava then
725+
if sym.is(Package)
726+
&& Feature.enabled(Feature.packageObjectValues)
727+
&& tree.tpe.member(nme.PACKAGE).hasAltWith(_.symbol.isPackageObject)
728+
then
729+
typed(untpd.Select(untpd.TypedSplice(tree), nme.PACKAGE))
730+
else
731+
report.error(SymbolIsNotAValue(sym), tree.srcPos)
732+
tree
733+
else tree
734+
735+
/** Check that `tree` refers to a value, unless `tree` is selected or applied
736+
* (singleton types x.type don't count as selections).
737+
*/
738+
def checkValue(tree: Tree, proto: Type)(using Context): Tree =
739+
tree match
740+
case tree: RefTree if tree.name.isTermName =>
741+
proto match
742+
case _: SelectionProto if proto ne SingletonTypeProto => tree // no value check
743+
case _: FunOrPolyProto => tree // no value check
744+
case _ => checkValue(tree)
745+
case _ => tree
746+
726747
/** (1) If this reference is neither applied nor selected, check that it does
727748
* not refer to a package or Java companion object.
728749
* (2) Check that a stable identifier pattern is indeed stable (SLS 8.1.5)
729750
*/
730-
private def checkLegalValue(tree: Tree, pt: Type)(using Context): Unit =
731-
checkValue(tree, pt)
751+
private def checkLegalValue(tree: Tree, pt: Type)(using Context): Tree =
752+
val tree1 = checkValue(tree, pt)
732753
if ctx.mode.is(Mode.Pattern)
733-
&& !tree.isType
754+
&& !tree1.isType
734755
&& !pt.isInstanceOf[ApplyingProto]
735-
&& !tree.tpe.match
756+
&& !tree1.tpe.match
736757
case tp: NamedType => tp.denot.hasAltWith(_.symbol.isStableMember && tp.prefix.isStable || tp.info.isStable)
737758
case tp => tp.isStable
738-
&& !isWildcardArg(tree)
759+
&& !isWildcardArg(tree1)
739760
then
740-
report.error(StableIdentPattern(tree, pt), tree.srcPos)
761+
report.error(StableIdentPattern(tree1, pt), tree1.srcPos)
762+
tree1
741763

742764
def typedSelectWithAdapt(tree0: untpd.Select, pt: Type, qual: Tree)(using Context): Tree =
743765
val selName = tree0.name
@@ -751,8 +773,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
751773
if checkedType.exists then
752774
val select = toNotNullTermRef(assignType(tree, checkedType), pt)
753775
if selName.isTypeName then checkStable(qual.tpe, qual.srcPos, "type prefix")
754-
checkLegalValue(select, pt)
755-
ConstFold(select)
776+
ConstFold(checkLegalValue(select, pt))
756777
else EmptyTree
757778

758779
// Otherwise, simplify `m.apply(...)` to `m(...)`
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
layout: doc-page
3+
title: "Reference-able Package Objects"
4+
redirectFrom: /docs/reference/experimental/package-object-values.html
5+
nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/package-object-values.html
6+
---
7+
8+
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
9+
```scala
10+
a.b.`package`
11+
```
12+
But this is ugly and non-obvious. Or one could use a normal `object`, which is not always possible.
13+
14+
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`.
15+
16+
The extension, turns the following into valid code:
17+
18+
```scala
19+
package a
20+
package object b
21+
22+
val z = a.b // Currently fails with "package is not a value"
23+
```
24+
25+
Currently the workaround is to use a `.package` suffix:
26+
27+
```scala
28+
val z = a.b.`package`
29+
```
30+
31+
With the extension, a reference such as `a.b` where `b` is a `package` containing a `package object`, expands to `a.b.package` automatically
32+
33+
## Limitations
34+
35+
* `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`.
36+
37+
* `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`
38+
39+
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.
40+
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
layout: doc-page
3+
title: "Toplevel Definitions"
4+
nightlyOf: https://docs.scala-lang.org/scala3/reference/dropped-features/toplevel-definitions.html
5+
---
6+
7+
All kind of definitions can now be written at the top-level.
8+
Example:
9+
```scala
10+
package p
11+
type Labelled[T] = (String, T)
12+
val a: Labelled[Int] = ("count", 1)
13+
def b = a._2
14+
15+
case class C()
16+
17+
extension (x: C) def pair(y: C) = (x, y)
18+
```
19+
Previously, `type`, `val` or `def` definitions had to be wrapped in a package object. Now,
20+
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.
21+
22+
The compiler generates synthetic objects that wrap top-level definitions falling into one of the following categories:
23+
24+
- all pattern, value, method, and type definitions,
25+
- implicit classes and objects,
26+
- companion objects of opaque type aliases.
27+
28+
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,
29+
after any other package clauses, imports, or object and class definitions.
30+
31+
**Note:** This means that
32+
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.
33+
34+
2. A top-level main method `def main(args: Array[String]): Unit = ...` is wrapped as any other method. If it appears
35+
in a source file `src.scala`, it could be invoked from the command line using a command like `scala src$package`. Since the
36+
"program name" is mangled it is recommended to always put `main` methods in explicitly named objects.
37+
38+
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.
39+
40+
4. If several top-level definitions are overloaded variants with the same name,
41+
they must all come from the same source file.

docs/sidebar.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@ subsection:
123123
- page: reference/dropped-features/type-projection.md
124124
- page: reference/dropped-features/do-while.md
125125
- page: reference/dropped-features/procedure-syntax.md
126-
- page: reference/dropped-features/package-objects.md
127126
- page: reference/dropped-features/early-initializers.md
128127
- page: reference/dropped-features/class-shadowing.md
129128
- page: reference/dropped-features/class-shadowing-spec.md
@@ -164,6 +163,7 @@ subsection:
164163
- page: reference/experimental/runtimeChecked.md
165164
- page: reference/experimental/better-fors.md
166165
- page: reference/experimental/unrolled-defs.md
166+
- page: reference/experimental/package-object-values.md
167167
- page: reference/syntax.md
168168
- title: Language Versions
169169
index: reference/language-versions/language-versions.md

library/src/scala/runtime/stdLibPatches/language.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ object language:
140140
*/
141141
@compileTimeOnly("`betterFors` can only be used at compile time in import statements")
142142
object betterFors
143+
144+
/** Experimental support for package object values
145+
*/
146+
@compileTimeOnly("`packageObjectValues` can only be used at compile time in import statements")
147+
object packageObjectValues
143148
end experimental
144149

145150
/** The deprecated object contains features that are no longer officially suypported in Scala.

project/MiMaFilters.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ object MiMaFilters {
1313
ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.quotedPatternsWithPolymorphicFunctions"),
1414
ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$quotedPatternsWithPolymorphicFunctions$"),
1515
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.runtime.Patterns.higherOrderHoleWithTypes"),
16+
ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.packageObjectValues"),
17+
ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$packageObjectValues$"),
1618
),
1719

1820
// Additions since last LTS

tests/run/pkgobjvals.check

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Foo was created
2+
Foo was created
3+
Foo was created
4+
Foo was created

tests/run/pkgobjvals.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import language.experimental.packageObjectValues
2+
3+
package a:
4+
package object b:
5+
class Foo:
6+
println("Foo was created")
7+
8+
def foo() = Foo()
9+
end b
10+
11+
def test =
12+
val bb = b
13+
bb.foo()
14+
new bb.Foo()
15+
end a
16+
17+
@main def Test =
18+
a.test
19+
val ab: a.b.type = a.b
20+
ab.foo()
21+
new ab.Foo()
22+

0 commit comments

Comments
 (0)