Skip to content

Allow applications in export qualifiers #14468

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3111,7 +3111,7 @@ object Parsers {
*/
def importClause(leading: Token, mkTree: ImportConstr): List[Tree] = {
val offset = accept(leading)
commaSeparated(importExpr(mkTree)) match {
commaSeparated(importExpr(mkTree, acceptArgs = leading == EXPORT)) match {
case t :: rest =>
// The first import should start at the start offset of the keyword.
val firstPos =
Expand Down Expand Up @@ -3142,6 +3142,8 @@ object Parsers {
imp

/** ImportExpr ::= SimpleRef {‘.’ id} ‘.’ ImportSpec
* | SimpleRef ‘as’ id
* ExportExpr ::= SimpleRef {‘.’ id | ParArgumentExprs} ‘.’ ImportSpec
* | SimpleRef ‘as’ id
* ImportSpec ::= NamedSelector
* | WildcardSelector
Expand All @@ -3151,7 +3153,7 @@ object Parsers {
* NamedSelector ::= id [‘as’ (id | ‘_’)]
* WildCardSelector ::= ‘*' | ‘given’ [InfixType]
*/
def importExpr(mkTree: ImportConstr): () => Tree =
def importExpr(mkTree: ImportConstr, acceptArgs: Boolean): () => Tree =

/** ‘*' | ‘_' */
def wildcardSelector() =
Expand Down Expand Up @@ -3216,6 +3218,8 @@ object Parsers {
mkTree(qual1, namedSelector(from) :: Nil)
case qual: Ident =>
mkTree(EmptyTree, namedSelector(qual) :: Nil)
else if acceptArgs && in.token == LPAREN then
importSelection(atSpan(startOffset(qual)) { mkApply(qual, parArgumentExprs()) })
else
accept(DOT)
in.token match
Expand Down
8 changes: 7 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/Checking.scala
Original file line number Diff line number Diff line change
Expand Up @@ -868,9 +868,15 @@ trait Checking {
then
report.error(em"no aliases can be used to refer to a language import", path.srcPos)

/** Check that `path` is a legal prefix for an export clause that exports a type */
def checkLegalExportPathForType(path: Tree, mbr: Symbol)(using Context): Unit =
checkLegalImportOrExportPath(path,
if !path.tpe.isStable // compute non-constant kind string only when we are sure that an error is issued
then i"export prefix for $mbr"
else "export prefix")

/** Check that `path` is a legal prefix for an export clause */
def checkLegalExportPath(path: Tree, selectors: List[untpd.ImportSelector])(using Context): Unit =
checkLegalImportOrExportPath(path, "export prefix")
if
selectors.exists(_.isWildcard)
&& path.tpe.classSymbol.is(PackageClass)
Expand Down
7 changes: 6 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/Namer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1150,7 +1150,11 @@ class Namer { typer: Typer =>
case tp: TermRef => tp.termSymbol.is(Private) || refersToPrivate(tp.prefix)
case _ => false
val (maybeStable, mbrInfo) =
if sym.isStableMember && sym.isPublic && !refersToPrivate(path.tpe) then
if sym.isStableMember
&& sym.isPublic
&& path.tpe.isStable
&& !refersToPrivate(path.tpe)
then
(StableRealizable, ExprType(path.tpe.select(sym)))
else
(EmptyFlags, mbr.info.ensureMethodic)
Expand All @@ -1165,6 +1169,7 @@ class Namer { typer: Typer =>
forwarder.addAnnotations(sym.annotations.filterConserve(_.symbol != defn.BodyAnnot))

if forwarder.isType then
checkLegalExportPathForType(path, sym)
buf += tpd.TypeDef(forwarder.asType).withSpan(span)
else
import tpd._
Expand Down
6 changes: 4 additions & 2 deletions docs/_docs/internals/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ SimpleExpr ::= SimpleRef
| SimpleExpr ‘_’ PostfixOp(expr, _) (to be dropped)
| XmlExpr -- to be dropped
IndentedExpr ::= indent CaseClauses | Block outdent
Quoted ::= ‘'’ ‘{’ Block ‘}’
Quoted ::= ‘'’ ‘{’ Block ‘}’
| ‘'’ ‘[’ Type ‘]’
ExprsInParens ::= ExprInParens {‘,’ ExprInParens}
ExprInParens ::= PostfixExpr ‘:’ Type -- normal Expr allows only RefinedType here
Expand Down Expand Up @@ -366,9 +366,11 @@ AccessQualifier ::= ‘[’ id ‘]’
Annotation ::= ‘@’ SimpleType1 {ParArgumentExprs} Apply(tpe, args)

Import ::= ‘import’ ImportExpr {‘,’ ImportExpr}
Export ::= ‘export’ ImportExpr {‘,’ ImportExpr}
ImportExpr ::= SimpleRef {‘.’ id} ‘.’ ImportSpec Import(expr, sels)
| SimpleRef ‘as’ id Import(EmptyTree, ImportSelector(ref, id))
Export ::= ‘export’ ExportExpr {‘,’ ExportExpr}
ExportExpr ::= SimpleRef {‘.’ id | ParArgumentExprs} ‘.’ ImportSpec Export(expr, sels)
| SimpleRef ‘as’ id Export(EmptyTree, ImportSelector(ref, id))
ImportSpec ::= NamedSelector
| WildcardSelector
| ‘{’ ImportSelectors) ‘}’
Expand Down
41 changes: 26 additions & 15 deletions docs/_docs/reference/other-new-features/export.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,22 @@ val copier = new Copier
copier.print(copier.scan())
```

An `export` clause has the same format as an import clause. Its general form is:
An `export` clause has a similar format as an import clause. Its general form is:

```scala
export path . { sel_1, ..., sel_n }
export qual . { sel_1, ..., sel_n }
```

It consists of a qualifier expression `path`, which must be a stable identifier, followed by
It consists of a qualifier expression `qual` followed by
one or more selectors `sel_i` that identify what gets an alias. Selectors can be
of one of the following forms:

- A _simple selector_ `x` creates aliases for all eligible members of `path` that are named `x`.
- A _renaming selector_ `x => y` creates aliases for all eligible members of `path` that are named `x`, but the alias is named `y` instead of `x`.
- A _simple selector_ `x` creates aliases for all eligible members of `qual` that are named `x`.
- A _renaming selector_ `x => y` creates aliases for all eligible members of `qual` that are named `x`, but the alias is named `y` instead of `x`.
- An _omitting selector_ `x => _` prevents `x` from being aliased by a subsequent
wildcard selector.
- A _given selector_ `given x` has an optional type bound `x`. It creates aliases for all eligible given instances that conform to either `x`, or `Any` if `x` is omitted, except for members that are named by a previous simple, renaming, or omitting selector.
- A _wildcard selector_ `*` creates aliases for all eligible members of `path` except for given instances,
- A _wildcard selector_ `*` creates aliases for all eligible members of `qual` except for given instances,
synthetic members generated by the compiler and those members that are named by a previous simple, renaming, or omitting selector.
\
Notes:
Expand All @@ -77,6 +77,18 @@ A member is _eligible_ if all of the following holds:

It is a compile-time error if a simple or renaming selector does not identify any eligible members.

The qualifier expression `qual` can contain selections as well as applications
to arguments. However, if a type member is exported, `qual` must be a stable path.

Example:

```scala
class C(x: Int) { type T; def m = x }
export C(2).m // OK, generates: def m = C(2).m
export C(3).T // error: need a path to export T
export C(4).* // also error since T is exported via *
```

Type members are aliased by type definitions, and term members are aliased by method definitions. 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:

Expand All @@ -85,8 +97,7 @@ Export aliases are always `final`. Aliases of given instances are again defined
not marked `override`.
- However, export aliases can implement deferred members of base classes.

Export aliases for public value definitions that are accessed without
referring to private values in the qualifier path
If the qualifier is a stable path, export aliases for public value definitions that are accessed without referring to private values in that path
are marked by the compiler as "stable" and their result types are the singleton types of the aliased definitions. This means that they can be used as parts of stable identifier paths, even though they are technically methods. For instance, the following is OK:
```scala
class C { type T }
Expand All @@ -99,7 +110,7 @@ def f: c.T = ...
**Restrictions:**

1. Export clauses can appear in classes or they can appear at the top-level. An export clause cannot appear as a statement in a block.
1. If an export clause contains a wildcard or given selector, it is forbidden for its qualifier path to refer to a package. This is because it is not yet known how to safely track wildcard dependencies to a package for the purposes of incremental compilation.
1. If an export clause contains a wildcard or given selector, it is forbidden for its qualifier to refer to a package. This is because it is not yet known how to safely track wildcard dependencies to a package for the purposes of incremental compilation.

1. Simple renaming exports like
```scala
Expand Down Expand Up @@ -130,8 +141,9 @@ TemplateStat ::= ...
| Export
TopStat ::= ...
| Export
Export ::= ‘export’ ImportExpr {‘,’ ImportExpr}
ImportExpr ::= SimpleRef {‘.’ id} ‘.’ ImportSpec
Export ::= ‘export’ ExportExpr {‘,’ ExportExpr}
ExportExpr ::= SimpleRef {‘.’ id | ParArgumentExprs} ‘.’ ImportSpec
| SimpleRef ‘as’ id
ImportSpec ::= NamedSelector
| WildcardSelector
| ‘{’ ImportSelectors) ‘}’
Expand Down Expand Up @@ -173,9 +185,8 @@ Export clauses are processed when the type information of the enclosing object o

With export clauses, the following steps are added:

6. Compute the types of all paths in export clauses.
7. Enter export aliases for the eligible members of all paths in export clauses.
6. Compute the types of all qualifiers in export clauses.
7. Enter export aliases for the eligible members of all qualifiers in export clauses.

It is important that steps 6 and 7 are done in sequence: We first compute the types of _all_
paths in export clauses and only after this is done we enter any export aliases as class members. This means that a path of an export clause cannot refer to an alias made available
by another export clause of the same class.
qualifiers in export clauses and only after this is done we enter any export aliases as class members. This means that a qualifier of an export clause cannot refer to an alias made available by another export clause of the same class.
4 changes: 3 additions & 1 deletion docs/_docs/reference/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,9 +355,11 @@ AccessQualifier ::= ‘[’ id ‘]’
Annotation ::= ‘@’ SimpleType1 {ParArgumentExprs}

Import ::= ‘import’ ImportExpr {‘,’ ImportExpr}
Export ::= ‘export’ ImportExpr {‘,’ ImportExpr}
ImportExpr ::= SimpleRef {‘.’ id} ‘.’ ImportSpec
| SimpleRef ‘as’ id
Export ::= ‘export’ ExportExpr {‘,’ ExportExpr}
ExportExpr ::= SimpleRef {‘.’ id | ParArgumentExprs} ‘.’ ImportSpec
| SimpleRef ‘as’ id
ImportSpec ::= NamedSelector
| WildcardSelector
| ‘{’ ImportSelectors) ‘}’
Expand Down
6 changes: 6 additions & 0 deletions tests/neg/exports.check
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,9 @@
| Double definition:
| val bar: Bar in class Baz at line 45 and
| final def bar: (Baz.this.bar.bar : => (Baz.this.bar.baz.bar : Bar)) in class Baz at line 46
-- [E083] Type Error: tests/neg/exports.scala:57:11 --------------------------------------------------------------------
57 | export printer("#1").* // error
| ^^^^^^^^^^^^^
| Printer is not a valid export prefix for type PrinterType, since it is not an immutable path
|
| longer explanation available when compiling with `-explain`
6 changes: 6 additions & 0 deletions tests/neg/exports.scala
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,9 @@
val baz: Baz = new Baz
export baz._
}

object No:
def printer(id: String) =
println(s"new Printer $id")
new Printer
export printer("#1").* // error
5 changes: 5 additions & 0 deletions tests/run/exports.check
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ config
printing
scanning
scanning
new Printer #1
printing
new Printer #1
printing
scanning
15 changes: 13 additions & 2 deletions tests/run/exports.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ object Test extends App {

object Copier {
val printer = new Printer
export printer.{given, _}
export Scanner.{scan => scanIt, _}
export printer.{given, *}
export Scanner.{scan => scanIt, *}

val config2 = summon[Config]
}
Expand All @@ -41,6 +41,17 @@ object Test extends App {
test()

val _: Int = B.x

object FunnyCopier:
def printer(id: String) =
println(s"new Printer $id")
new Printer
export printer("#1").*
export Scanner.*

FunnyCopier.print()
FunnyCopier.print()
FunnyCopier.scan()
}

final class Foo {
Expand Down