diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 243e4b789ba2..90bf2255105a 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1033,7 +1033,7 @@ object desugar { case stat: TypeDef if stat.mods.is(Opaque) => stat.name } def needsObject(stat: Tree) = stat match { - case _: ValDef | _: PatDef | _: DefDef => true + case _: ValDef | _: PatDef | _: DefDef | _: Export => true case stat: ModuleDef => stat.mods.is(ImplicitOrImplied) || opaqueNames.contains(stat.name.stripModuleClassSuffix.toTypeName) case stat: TypeDef => !stat.isClassDef || stat.mods.is(ImplicitOrImplied) diff --git a/compiler/src/dotty/tools/dotc/ast/Trees.scala b/compiler/src/dotty/tools/dotc/ast/Trees.scala index 19925e5eb60d..65e604d8fce0 100644 --- a/compiler/src/dotty/tools/dotc/ast/Trees.scala +++ b/compiler/src/dotty/tools/dotc/ast/Trees.scala @@ -1403,7 +1403,7 @@ object Trees { this(x, rhs) case tree @ Template(constr, parents, self, _) if tree.derived.isEmpty => this(this(this(this(x, constr), parents), self), tree.body) - case Import(importImplied, expr, selectors) => + case Import(_, expr, _) => this(x, expr) case PackageDef(pid, stats) => this(this(x, pid), stats)(localCtx) diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index f95e4b37adce..55a1cbfeff73 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -103,6 +103,9 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case class GenAlias(pat: Tree, expr: Tree)(implicit @constructorOnly src: SourceFile) extends Tree case class ContextBounds(bounds: TypeBoundsTree, cxBounds: List[Tree])(implicit @constructorOnly src: SourceFile) extends TypTree case class PatDef(mods: Modifiers, pats: List[Tree], tpt: Tree, rhs: Tree)(implicit @constructorOnly src: SourceFile) extends DefTree + case class Export(impliedOnly: Boolean, expr: Tree, selectors: List[Tree])(implicit @constructorOnly src: SourceFile) extends Tree + + /** Short-lived usage in typer, does not need copy/transform/fold infrastructure */ case class DependentTypeTree(tp: List[Symbol] => Type)(implicit @constructorOnly src: SourceFile) extends Tree @sharable object EmptyTypeIdent extends Ident(tpnme.EMPTY)(NoSource) with WithoutTypeOrPos[Untyped] { @@ -532,6 +535,10 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case tree: PatDef if (mods eq tree.mods) && (pats eq tree.pats) && (tpt eq tree.tpt) && (rhs eq tree.rhs) => tree case _ => finalize(tree, untpd.PatDef(mods, pats, tpt, rhs)(tree.source)) } + def Export(tree: Tree)(impliedOnly: Boolean, expr: Tree, selectors: List[Tree])(implicit ctx: Context): Tree = tree match { + case tree: Export if (impliedOnly == tree.impliedOnly) && (expr eq tree.expr) && (selectors eq tree.selectors) => tree + case _ => finalize(tree, untpd.Export(impliedOnly, expr, selectors)(tree.source)) + } def TypedSplice(tree: Tree)(splice: tpd.Tree)(implicit ctx: Context): ProxyTree = tree match { case tree: TypedSplice if splice `eq` tree.splice => tree case _ => finalize(tree, untpd.TypedSplice(splice)(ctx)) @@ -584,6 +591,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { cpy.ContextBounds(tree)(transformSub(bounds), transform(cxBounds)) case PatDef(mods, pats, tpt, rhs) => cpy.PatDef(tree)(mods, transform(pats), transform(tpt), transform(rhs)) + case Export(impliedOnly, expr, selectors) => + cpy.Export(tree)(impliedOnly, transform(expr), selectors) case TypedSplice(_) => tree case _ => @@ -637,6 +646,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { this(this(x, bounds), cxBounds) case PatDef(mods, pats, tpt, rhs) => this(this(this(x, pats), tpt), rhs) + case Export(_, expr, _) => + this(x, expr) case TypedSplice(splice) => this(x, splice) case _ => diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 6f0250f3ccdf..d74bbfff8ab0 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2225,13 +2225,16 @@ object Parsers { def finalizeDef(md: MemberDef, mods: Modifiers, start: Int): md.ThisTree[Untyped] = md.withMods(mods).setComment(in.getDocComment(start)) + type ImportConstr = (Boolean, Tree, List[Tree]) => Tree + /** Import ::= import [implied] [ImportExpr {`,' ImportExpr} + * Export ::= export [implied] [ImportExpr {`,' ImportExpr} */ - def importClause(): List[Tree] = { - val offset = accept(IMPORT) + def importClause(leading: Token, mkTree: ImportConstr): List[Tree] = { + val offset = accept(leading) val importImplied = in.token == IMPLIED if (importImplied) in.nextToken() - commaSeparated(importExpr(importImplied)) match { + commaSeparated(importExpr(importImplied, mkTree)) match { case t :: rest => // The first import should start at the start offset of the keyword. val firstPos = @@ -2244,23 +2247,28 @@ object Parsers { /** ImportExpr ::= StableId `.' (id | `_' | ImportSelectors) */ - def importExpr(importImplied: Boolean): () => Import = { + def importExpr(importImplied: Boolean, mkTree: ImportConstr): () => Tree = { val handleImport: Tree => Tree = { tree: Tree => - if (in.token == USCORE) Import(importImplied, tree, importSelector() :: Nil) - else if (in.token == LBRACE) Import(importImplied, tree, inBraces(importSelectors())) + if (in.token == USCORE) mkTree(importImplied, tree, importSelector() :: Nil) + else if (in.token == LBRACE) mkTree(importImplied, tree, inBraces(importSelectors())) else tree } - () => path(thisOK = false, handleImport) match { - case imp: Import => - imp - case sel @ Select(qual, name) => - val selector = atSpan(pointOffset(sel)) { Ident(name) } - cpy.Import(sel)(importImplied, qual, selector :: Nil) - case t => - accept(DOT) - Import(importImplied, t, Ident(nme.WILDCARD) :: Nil) + def derived(impExp: Tree, qual: Tree, selectors: List[Tree]) = + mkTree(importImplied, qual, selectors).withSpan(impExp.span) + + () => { + val p = path(thisOK = false, handleImport) + p match { + case _: Import | _: Export => p + case sel @ Select(qual, name) => + val selector = atSpan(pointOffset(sel)) { Ident(name) } + mkTree(importImplied, qual, selector :: Nil).withSpan(sel.span) + case t => + accept(DOT) + mkTree(importImplied, t, Ident(nme.WILDCARD) :: Nil) + } } } @@ -2769,7 +2777,9 @@ object Parsers { else stats += packaging(start) } else if (in.token == IMPORT) - stats ++= importClause() + stats ++= importClause(IMPORT, Import) + else if (in.token == EXPORT) + stats ++= importClause(EXPORT, Export.apply) else if (in.token == AT || isDefIntro(modifierTokens)) stats +++= defOrDcl(in.offset, defAnnotsMods(modifierTokens)) else if (!isStatSep) { @@ -2816,7 +2826,9 @@ object Parsers { while (!isStatSeqEnd && !exitOnError) { setLastStatOffset() if (in.token == IMPORT) - stats ++= importClause() + stats ++= importClause(IMPORT, Import) + else if (in.token == EXPORT) + stats ++= importClause(EXPORT, Export.apply) else if (isExprIntro) stats += expr1() else if (isDefIntro(modifierTokensOrCase)) @@ -2888,7 +2900,7 @@ object Parsers { while (!isStatSeqEnd && in.token != CASE && !exitOnError) { setLastStatOffset() if (in.token == IMPORT) - stats ++= importClause() + stats ++= importClause(IMPORT, Import) else if (in.token == GIVEN) stats += implicitClosure(in.offset, Location.InBlock, modifiers(closureMods)) else if (isExprIntro) diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index 936663eb9b14..56b5e8b825ac 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -180,7 +180,8 @@ object Tokens extends TokensCommon { final val ERASED = 63; enter(ERASED, "erased") final val IMPLIED = 64; enter(IMPLIED, "implied") final val GIVEN = 65; enter(GIVEN, "given") - final val MACRO = 66; enter(MACRO, "macro") // TODO: remove + final val EXPORT = 66; enter(EXPORT, "export") + final val MACRO = 67; enter(MACRO, "macro") // TODO: remove /** special symbols */ final val NEWLINE = 78; enter(NEWLINE, "end of statement", "new line") @@ -240,7 +241,7 @@ object Tokens extends TokensCommon { final val modifierFollowers = modifierTokens | defIntroTokens /** Is token only legal as start of statement (eof also included)? */ - final val mustStartStatTokens: TokenSet = defIntroTokens | modifierTokens | BitSet(IMPORT, PACKAGE) + final val mustStartStatTokens: TokenSet = defIntroTokens | modifierTokens | BitSet(IMPORT, EXPORT, PACKAGE) final val canStartStatTokens: TokenSet = canStartExpressionTokens | mustStartStatTokens | BitSet( AT, CASE) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 8d197bf7fbf3..9fbdcbbc0606 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -594,6 +594,12 @@ trait Checking { ctx.error(ex"$cls cannot be instantiated since it${rstatus.msg}", pos) } + /** Check that `path` is a legal prefix for an import or export clause */ + def checkLegalImportPath(path: Tree)(implicit ctx: Context): Unit = { + checkStable(path.tpe, path.sourcePos) + if (!ctx.isAfterTyper) Checking.checkRealizable(path.tpe, path.posd) + } + /** Check that `tp` is a class type. * Also, if `traitReq` is true, check that `tp` is a trait. * Also, if `stablePrefixReq` is true and phase is not after RefChecks, diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 9986be9631c1..e3f299d8f833 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -195,6 +195,7 @@ class Namer { typer: Typer => val TypedAhead: Property.Key[tpd.Tree] = new Property.Key val ExpandedTree: Property.Key[untpd.Tree] = new Property.Key + val ExportForwarders: Property.Key[List[tpd.MemberDef]] = new Property.Key val SymOfTree: Property.Key[Symbol] = new Property.Key val Deriver: Property.Key[typer.Deriver] = new Property.Key @@ -932,6 +933,120 @@ class Namer { typer: Typer => def init(): Context = index(params) + /** Add forwarders as required by the export statements in this class */ + private def processExports(implicit ctx: Context): Unit = { + + /** A string indicating that no forwarders for this kind of symbol are emitted */ + val SKIP = "(skip)" + + /** The forwarders defined by export `exp`. + */ + def exportForwarders(exp: Export): List[tpd.MemberDef] = { + val buf = new mutable.ListBuffer[tpd.MemberDef] + val Export(_, expr, selectors) = exp + val path = typedAheadExpr(expr, AnySelectionProto) + checkLegalImportPath(path) + + def whyNoForwarder(mbr: SingleDenotation): String = { + val sym = mbr.symbol + if (sym.is(ImplicitOrImplied) != exp.impliedOnly) s"is ${if (exp.impliedOnly) "not " else ""}implied" + else if (!sym.isAccessibleFrom(path.tpe)) "is not accessible" + else if (sym.isConstructor || sym.is(ModuleClass) || sym.is(Bridge)) SKIP + else if (cls.derivesFrom(sym.owner) && + (sym.owner == cls || !sym.is(Deferred))) i"is already a member of $cls" + else "" + } + + /** Add a forwarder with name `alias` or its type name equivalent to `mbr`, + * provided `mbr` is accessible and of the right implicit/non-implicit kind. + */ + def addForwarder(alias: TermName, mbr: SingleDenotation, span: Span): Unit = { + if (whyNoForwarder(mbr) == "") { + + /** The info of a forwarder to type `ref` which has info `info` + */ + def fwdInfo(ref: Type, info: Type): Type = info match { + case _: ClassInfo => + HKTypeLambda.fromParams(info.typeParams, ref) + case _: TypeBounds => + TypeAlias(ref) + case info: HKTypeLambda => + info.derivedLambdaType(info.paramNames, info.paramInfos, + fwdInfo(ref.appliedTo(info.paramRefs), info.resultType)) + case info => // should happen only in error cases + info + } + + val forwarder = + if (mbr.isType) + ctx.newSymbol( + cls, alias.toTypeName, + Final, + fwdInfo(path.tpe.select(mbr.symbol), mbr.info), + coord = span) + else { + val maybeStable = if (mbr.symbol.isStableMember) StableRealizable else EmptyFlags + ctx.newSymbol( + cls, alias, + Method | Final | maybeStable | mbr.symbol.flags & ImplicitOrImplied, + mbr.info.ensureMethodic, + coord = span) + } + val forwarderDef = + if (forwarder.isType) tpd.TypeDef(forwarder.asType) + else { + import tpd._ + val ref = path.select(mbr.symbol.asTerm) + tpd.polyDefDef(forwarder.asTerm, targs => prefss => + ref.appliedToTypes(targs).appliedToArgss(prefss) + ) + } + buf += forwarderDef.withSpan(span) + } + } + + def addForwardersNamed(name: TermName, alias: TermName, span: Span): Unit = { + val size = buf.size + val mbrs = List(name, name.toTypeName).flatMap(path.tpe.member(_).alternatives) + mbrs.foreach(addForwarder(alias, _, span)) + if (buf.size == size) { + val reason = mbrs.map(whyNoForwarder).dropWhile(_ == SKIP) match { + case Nil => "" + case why :: _ => i"\n$path.$name cannot be exported because it $why" + } + ctx.error(i"""no eligible member $name at $path$reason""", ctx.source.atSpan(span)) + } + } + + def addForwardersExcept(seen: List[TermName], span: Span): Unit = + for (mbr <- path.tpe.allMembers) { + val alias = mbr.name.toTermName + if (!seen.contains(alias)) addForwarder(alias, mbr, span) + } + + def recur(seen: List[TermName], sels: List[untpd.Tree]): Unit = sels match { + case (sel @ Ident(nme.WILDCARD)) :: _ => + addForwardersExcept(seen, sel.span) + case (sel @ Ident(name: TermName)) :: rest => + addForwardersNamed(name, name, sel.span) + recur(name :: seen, rest) + case Thicket((sel @ Ident(fromName: TermName)) :: Ident(toName: TermName) :: Nil) :: rest => + if (toName != nme.WILDCARD) addForwardersNamed(fromName, toName, sel.span) + recur(fromName :: seen, rest) + case _ => + } + + recur(Nil, selectors) + val forwarders = buf.toList + exp.pushAttachment(ExportForwarders, forwarders) + forwarders + } + + val forwarderss = + for (exp @ Export(_, _, _) <- rest) yield exportForwarders(exp) + forwarderss.foreach(_.foreach(fwdr => fwdr.symbol.entered)) + } + /** The type signature of a ClassDef with given symbol */ override def completeInCreationContext(denot: SymDenotation): Unit = { val parents = impl.parents @@ -1074,6 +1189,7 @@ class Namer { typer: Typer => cls.baseClasses.foreach(_.invalidateBaseTypeCache()) // we might have looked before and found nothing cls.setNoInitsFlags(parentsKind(parents), bodyKind(rest)) if (cls.isNoInitsClass) cls.primaryConstructor.setFlag(StableRealizable) + processExports(localCtx) } } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 1c81ef0ec665..4edc94308703 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1780,8 +1780,7 @@ class Typer extends Namer def typedImport(imp: untpd.Import, sym: Symbol)(implicit ctx: Context): Import = track("typedImport") { val expr1 = typedExpr(imp.expr, AnySelectionProto) - checkStable(expr1.tpe, imp.expr.sourcePos) - if (!ctx.isAfterTyper) checkRealizable(expr1.tpe, imp.expr.posd) + checkLegalImportPath(expr1) assignType(cpy.Import(imp)(imp.importImplied, expr1, imp.selectors), sym) } @@ -2205,6 +2204,10 @@ class Typer extends Namer } case Thicket(stats) :: rest => traverse(stats ++ rest) + case (stat: untpd.Export) :: rest => + buf ++= stat.attachmentOrElse(ExportForwarders, Nil) + // no attachment can happen in case of cyclic references + traverse(rest) case stat :: rest => val stat1 = typed(stat)(ctx.exprContext(stat, exprOwner)) checkStatementPurity(stat1)(stat, exprOwner) diff --git a/docs/docs/internals/syntax.md b/docs/docs/internals/syntax.md index 74a9d33715d1..bd0eada17306 100644 --- a/docs/docs/internals/syntax.md +++ b/docs/docs/internals/syntax.md @@ -334,6 +334,7 @@ Import ::= ‘import’ [‘implied’] ImportExpr {‘,’ ImportEx ImportExpr ::= StableId ‘.’ (id | ‘_’ | ImportSelectors) Import(expr, sels) ImportSelectors ::= ‘{’ {ImportSelector ‘,’} (ImportSelector | ‘_’) ‘}’ ImportSelector ::= id [‘=>’ id | ‘=>’ ‘_’] Ident(name), Pair(id, id) +Export ::= ‘export’ [‘implied’] ImportExpr {‘,’ ImportExpr} ``` ### Declarations and Definitions @@ -370,7 +371,8 @@ DefDef ::= DefSig [(‘:’ | ‘<:’) Type] ‘=’ Expr TmplDef ::= ([‘case’] ‘class’ | ‘trait’) ClassDef | [‘case’] ‘object’ ObjectDef | ‘enum’ EnumDef - | ‘witness’ WitnessDef + | ‘implied’ InstanceDef + | Export ClassDef ::= id ClassConstr [Template] ClassDef(mods, name, tparams, templ) ClassConstr ::= [ClsTypeParamClause] [ConstrMods] ClsParamClauses with DefDef(_, , Nil, vparamss, EmptyTree, EmptyTree) as first stat ConstrMods ::= {Annotation} [AccessModifier] @@ -406,6 +408,7 @@ EnumCase ::= ‘case’ (id ClassConstr [‘extends’ ConstrApps]] | TopStatSeq ::= TopStat {semi TopStat} TopStat ::= Import + | Export | {Annotation [nl]} {Modifier} Def | Packaging | PackageObject diff --git a/docs/docs/reference/other-new-features/export.md b/docs/docs/reference/other-new-features/export.md new file mode 100644 index 000000000000..48bc2a43ab3a --- /dev/null +++ b/docs/docs/reference/other-new-features/export.md @@ -0,0 +1,147 @@ +--- +layout: doc-page +title: "Export" +--- + +An export clause defines aliases for selected members of an object. Example: +```scala + class BitMap + class InkJet + + class Printer { + type PrinterType + def print(bits: BitMap): Unit = ??? + def status: List[String] = ??? + } + + class Scanner { + def scan(): BitMap = ??? + def status: List[String] = ??? + } + + class Copier { + private val printUnit = new Printer { type PrinterType = InkJet } + private val scanUnit = new Scanner + + export scanUnit.scan + export printUnit.{status => _, _} + + def status: List[String] = printUnit.status ++ scanUnit.status + } +``` +The two `export` clauses define the following _export aliases_ in class `Copier`: +```scala + final def scan(): BitMap = scanUnit.scan() + final def print(bits: BitMap): Unit = printUnit.print(bits) + final type PrinterType = printUnit.PrinterType +``` +They can be accessed inside `Copier` as well as from outside: +```scala + val copier = new Copier + copier.print(copier.scan()) +``` +An export clause has the same format as an import clause. Its general form is: +```scala + export path . { sel_1, ..., sel_n } + export implied path . { sel_1, ..., sel_n } +``` +It consists of a qualifier expression `path`, which must be a stable identifier, 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`. + - An _omitting selector_ `x => _` prevents `x` from being aliased by a subsequent + wildcard selector. + - A _wildcard selector_ creates aliases for all eligible members of `path` except for + those members that are named by a previous simple, renaming, or omitting selector. + +A member is _eligible_ if all of the following holds: + + - its owner is not a base class of the class(*) containing the export clause, + - it is accessible at the export clause, + - it is not a constructor, nor the (synthetic) class part of an object, + - it is an `implied` instance (or an old-style `implicit` value) + if and only if the export is `implied`. + +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. +Export aliases are always `final`. Aliases of implied instances are again `implied` (and aliases of old-style implicits are `implicit`). 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 value definitions are marked by the compiler as "stable". 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 } + object O { val c: C = ... } + export O.c + def f: c.T = ... +``` + +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. + +(*) Note: Unless otherwise stated, the term "class" in this discussion also includes object and trait definitions. + +### Motivation + +It is a standard recommendation to prefer aggregation over inheritance. This is really an application of the principle of least power: Aggregation treats components as blackboxes whereas inheritance can affect the internal workings of components through overriding. Sometimes the close coupling implied by inheritance is the best solution for a problem, but where this is not necessary the looser coupling of aggregation is better. + +So far, object oriented languages including Scala made it much easer to use inheritance than aggregation. Inheritance only requires an `extends` clause whereas aggregation required a verbose elaboration of a sequence of forwarders. So in that sense, OO languages are pushing +programmers to a solution that is often too powerful. Export clauses redress the balance. They make aggregation relationships as concise and easy to express as inheritance relationships. Export clauses also offer more flexibility than extends clauses since members can be renamed or omitted. + +Export clauses also fill a gap opened by the shift from package objects to toplevel definitions. One occasionally useful idiom that gets lost in this shift is a package object inheriting from some class. The idiom is often used in a facade like pattern, to make members +of internal compositions available to users of a package. Toplevel definitions are not wrapped in a user-defined object, so they can't inherit anything. However, toplevel definitions can be export clauses, which supports the facade design pattern in a safer and +more flexible way. + +### Syntax changes: + +``` +TemplateStat ::= ... + | Export +TopStat ::= ... + | Export +Export ::= ‘export’ [‘implied’] ImportExpr {‘,’ ImportExpr} +``` + +### Elaboration of Export Clauses + +Export clauses raise questions about the order of elaboration during type checking. +Consider the following example: +```scala + class B { val c: Int } + object a { val b = new B } + export a._ + export b._ +} +``` +Is the `export b._` clause legal? If yes, what does it export? Is it equivalent to `export a.b._`? What about if we swap the last two clauses? +``` + export b._ + export a._ +``` +To avoid tricky questions like these, we fix the elaboration order of exports as follows. + +Export clauses are processed when the type information of the enclosing object or class is completed. Completion so far consisted of the following steps: + + 1. Elaborate any annotations of the class. + 2. Elaborate the parameters of the class. + 3. Elaborate the self type of the class, if one is given. + 4. Enter all definitions of the class as class members, with types to be completed + on demand. + 5. Determine the types of all parents of the class. + +With export clauses, the following steps are added: + + 6. Compute the types of all paths in export clauses in a context logically + inside the class but not considering any imports or exports in that class. + 7. Enter export aliases for the eligible members of all paths 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. diff --git a/tests/neg/exports.check b/tests/neg/exports.check new file mode 100644 index 000000000000..b5fd10abb817 --- /dev/null +++ b/tests/neg/exports.check @@ -0,0 +1,29 @@ +<1150..1150> in exports.scala +Cyclic reference involving value bar +[1091..1094] in exports.scala +no eligible member foo at this.foo +this.foo.foo cannot be exported because it is already a member of class Foo +[991..997] in exports.scala +no eligible member concat at this +this.concat cannot be exported because it is already a member of trait IterableOps +<647..647> in exports.scala +Double definition: +final def status: => List[String] in class Copier at line 23 and +final def status: => List[String] in class Copier at line 24 +have the same type after erasure. +<596..596> in exports.scala +Double definition: +def status: => List[String] in class Copier at line 28 and +final def status: => List[String] in class Copier at line 23 +have the same type after erasure. +[785..791] in exports.scala +no eligible member status at this.printUnit +this.printUnit.status cannot be exported because it is not implied +[712..718] in exports.scala +no eligible member bitmap at this.printUnit +this.printUnit.bitmap cannot be exported because it is implied +[518..525] in exports.scala +no eligible member scanAll at this.scanUnit +this.scanUnit.scanAll cannot be exported because it is not accessible +[452..458] in exports.scala +no eligible member scanIt at this.scanUnit diff --git a/tests/neg/exports.scala b/tests/neg/exports.scala new file mode 100644 index 000000000000..2f5f583d39ff --- /dev/null +++ b/tests/neg/exports.scala @@ -0,0 +1,51 @@ + class BitMap + class InkJet + + class Printer { + type PrinterType + def print(bits: BitMap): Unit = ??? + def status: List[String] = ??? + implied bitmap for BitMap + } + + class Scanner { + def scan(): BitMap = ??? + private def scanAll: BitMap = ??? + def status: List[String] = ??? + } + + class Copier { + private val printUnit = new Printer { type PrinterType = InkJet } + private val scanUnit = new Scanner + + export scanUnit.scanIt // error: no eligible member + export scanUnit.{scanAll => foo} // error: no eligible member + export printUnit.{stat => _, _} // error: double definition + export scanUnit._ // error: double definition + export printUnit.bitmap // error: no eligible member + export implied printUnit.status // error: no eligible member + + def status: List[String] = printUnit.status ++ scanUnit.status + } + +trait IterableOps[+A, +CC[_], +C] { + + def concat[B >: A](other: List[B]): CC[B] + + export this.{concat => ++} // error: no eligible member + +} + +class Foo { + val foo : Foo = new Foo + export foo.foo // error: no eligible member +} + +class Baz { + val bar: Bar = new Bar // error: cyclic reference + export bar._ +} +class Bar { + val baz: Baz = new Baz + export baz._ +} diff --git a/tests/pos/export-proxies.scala b/tests/pos/export-proxies.scala new file mode 100644 index 000000000000..0b983dccc649 --- /dev/null +++ b/tests/pos/export-proxies.scala @@ -0,0 +1,15 @@ +trait Session{ + def call1(): Unit + def call2(): Unit + def call3(): Unit + type T +} + +class SessionProxy(val session:Session) extends Session { + export session.{call2, call3, T} + + def call1(): Unit = { + println("call1") + session.call1() + } +} \ No newline at end of file diff --git a/tests/pos/reference/exports.scala b/tests/pos/reference/exports.scala new file mode 100644 index 000000000000..fe924c57e489 --- /dev/null +++ b/tests/pos/reference/exports.scala @@ -0,0 +1,28 @@ + class BitMap + class InkJet + + class Printer { + type PrinterType + def print(bits: BitMap): Unit = ??? + def status: List[String] = ??? + } + + class Scanner { + def scan(): BitMap = ??? + def status: List[String] = ??? + } + + class Copier { + private val printUnit = new Printer { type PrinterType = InkJet } + private val scanUnit = new Scanner + + export scanUnit.scan + export printUnit.{status => _, _} + + def status: List[String] = printUnit.status ++ scanUnit.status + } + + class C22 { type T } + object O22 { val c: C22 = ??? } + export O22.c + def f22: c.T = ??? diff --git a/tests/run/exports.check b/tests/run/exports.check new file mode 100644 index 000000000000..699259999e5a --- /dev/null +++ b/tests/run/exports.check @@ -0,0 +1,4 @@ +config +printing +scanning +config diff --git a/tests/run/exports.scala b/tests/run/exports.scala new file mode 100644 index 000000000000..489abe281dfb --- /dev/null +++ b/tests/run/exports.scala @@ -0,0 +1,37 @@ +object Test extends App { + + case class Config() { + println("config") + } + + class Printer { + def print() = println("printing") + object cfg extends Config + implied config for Config + } + + class Scanner { + def scan() = println("scanning") + } + object Scanner extends Scanner + + object Copier { + val printer = new Printer + export printer._ + export implied printer._ + export Scanner.{scan => scanIt, _} + + val config2 = the[Config] + } + + Copier.print() + Copier.scanIt() + Copier.cfg + Copier.config + Copier.config2 +} + +final class Foo { + lazy val foo : Foo = new Foo + export foo._ // nothing is exported +} \ No newline at end of file