Skip to content

Commit

Permalink
Expand value references to packages to their underlying package objects
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
odersky committed Nov 22, 2024
1 parent 8fb27f1 commit 97fa8ff
Show file tree
Hide file tree
Showing 10 changed files with 81 additions and 46 deletions.
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/config/Feature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions compiler/src/dotty/tools/dotc/reporting/messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
|"""

Expand All @@ -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):
Expand Down
6 changes: 3 additions & 3 deletions compiler/src/dotty/tools/dotc/transform/Erasure.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down
18 changes: 0 additions & 18 deletions compiler/src/dotty/tools/dotc/typer/Checking.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 36 additions & 15 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 =>
Expand Down Expand Up @@ -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
Expand All @@ -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(...)`
Expand Down
5 changes: 5 additions & 0 deletions library/src/scala/runtime/stdLibPatches/language.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions tests/neg/i21696.check
Original file line number Diff line number Diff line change
Expand Up @@ -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.
---------------------------------------------------------------------------------------------------------------------
4 changes: 4 additions & 0 deletions tests/run/pkgobjvals.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Foo was created
Foo was created
Foo was created
Foo was created
22 changes: 22 additions & 0 deletions tests/run/pkgobjvals.scala
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit 97fa8ff

Please sign in to comment.