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 all commits
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
20 changes: 13 additions & 7 deletions compiler/src/dotty/tools/dotc/typer/Namer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1064,14 +1064,14 @@ class Namer { typer: Typer =>
def init(): Context = index(params)

/** The forwarders defined by export `exp` */
private def exportForwarders(exp: Export)(using Context): List[tpd.MemberDef] =
private def exportForwarders(exp: Export, localDummy: Symbol)(using Context): List[tpd.MemberDef] =
val buf = new mutable.ListBuffer[tpd.MemberDef]
val Export(expr, selectors) = exp
if expr.isEmpty then
report.error(em"Export selector must have prefix and `.`", exp.srcPos)
return Nil

val path = typedAheadExpr(expr, AnySelectionProto)
val path = typedAheadExpr(expr, AnySelectionProto)(using ctx.withOwner(localDummy))
checkLegalExportPath(path, selectors)
lazy val wildcardBound = importBound(selectors, isGiven = false)
lazy val givenBound = importBound(selectors, isGiven = true)
Expand Down 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,10 +1169,11 @@ 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._
val ref = path.select(sym.asTerm)
val ref = path.changeOwner(localDummy, forwarder).select(sym.asTerm)
val ddef = tpd.DefDef(forwarder.asTerm, prefss =>
ref.appliedToArgss(adaptForwarderParams(Nil, sym.info, prefss)))
if forwarder.isInlineMethod then
Expand Down Expand Up @@ -1234,11 +1239,11 @@ class Namer { typer: Typer =>
end exportForwarders

/** Add forwarders as required by the export statements in this class */
private def processExports(using Context): Unit =
private def processExports(localDummy: Symbol)(using Context): Unit =

def process(stats: List[Tree])(using Context): Unit = stats match
case (stat: Export) :: stats1 =>
for forwarder <- exportForwarders(stat) do
for forwarder <- exportForwarders(stat, localDummy) do
forwarder.symbol.entered
process(stats1)
case (stat: Import) :: stats1 =>
Expand Down Expand Up @@ -1435,7 +1440,8 @@ class Namer { typer: Typer =>
if cls.is(Trait) then cls.is(NoInits)
else cls.isNoInitsRealClass
if ctorStable then cls.primaryConstructor.setFlag(StableRealizable)
processExports(using localCtx)
val localDummy = recordSym(newLocalDummy(cls, impl.span), impl)
processExports(localDummy)(using localCtx)
defn.patchStdLibClass(cls)
addConstructorProxies(cls)
}
Expand Down
12 changes: 6 additions & 6 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1229,7 +1229,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
if pos < mtpe.paramInfos.length then
mtpe.paramInfos(pos)
// This works only if vararg annotations match up.
// See neg/i14367.scala for an example where the inferred type is mispredicted.
// See neg/i14367.scala for an example where the inferred type is mispredicted.
// Nevertheless, the alternative would be to give up completely, so this is
// defensible.
else NoType
Expand Down Expand Up @@ -2530,7 +2530,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
parent

def localDummy(cls: ClassSymbol, impl: untpd.Template)(using Context): Symbol =
newLocalDummy(cls, impl.span)
impl.removeAttachment(SymOfTree).get

inline private def typedSelectors(selectors: List[untpd.ImportSelector])(using Context): List[untpd.ImportSelector] =
selectors.mapConserve { sel =>
Expand Down Expand Up @@ -2565,8 +2565,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
val selectors1 = typedSelectors(imp.selectors)
assignType(cpy.Import(imp)(expr1, selectors1), sym)

def typedExport(exp: untpd.Export)(using Context): Export =
val expr1 = typedExpr(exp.expr, AnySelectionProto)
def typedExport(exp: untpd.Export, exprOwner: Symbol)(using Context): Export =
val expr1 = typedExpr(exp.expr, AnySelectionProto)(using ctx.withOwner(exprOwner))
// already called `checkLegalExportPath` in Namer
val selectors1 = typedSelectors(exp.selectors)
assignType(cpy.Export(exp)(expr1, selectors1))
Expand Down Expand Up @@ -2822,7 +2822,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
case tree: untpd.Function => typedFunction(tree, pt)
case tree: untpd.Closure => typedClosure(tree, pt)
case tree: untpd.Import => typedImport(tree, retrieveSym(tree))
case tree: untpd.Export => typedExport(tree)
case tree: untpd.Export => typedExport(tree, ctx.owner)
case tree: untpd.Match => typedMatch(tree, pt)
case tree: untpd.Return => typedReturn(tree)
case tree: untpd.WhileDo => typedWhileDo(tree)
Expand Down Expand Up @@ -2982,7 +2982,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
case Thicket(stats) :: rest =>
traverse(stats ::: rest)
case (stat: untpd.Export) :: rest =>
buf += typed(stat)
buf += typedExport(stat, exprOwner)
buf ++= stat.attachmentOrElse(ExportForwarders, Nil)
// no attachment can happen in case of cyclic references
traverse(rest)
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
54 changes: 38 additions & 16 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,16 +77,38 @@ 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.

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.
Type members are aliased by type definitions, and term members are aliased by method definitions. For instance:
```scala
object O:
class C(val x: Int)
def m(c: C): Int = c.x + 1
export O.*
// generates
// type C = O.C
// def m(c: O.C): Int = O.m(c)
```
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 *
```


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 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 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 +121,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 +152,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 +196,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
7 changes: 7 additions & 0 deletions tests/pos/export-macros/A_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import quoted.*
class Container1(arg: Int):
object Internal1:
def exec: Unit = println("this is Internal1")
transparent inline def myC: Any = ${ macroCrap }
def macroCrap(using Quotes): Expr[Any] =
'{ Container1(1) }
1 change: 1 addition & 0 deletions tests/pos/export-macros/B_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export myC.*
7 changes: 7 additions & 0 deletions tests/pos/export-transparent.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Container(args: Any*):
object Internal

object Test:
transparent inline def myC(args: Any*): Container = Container(args)

export myC(1).*
Loading