From 3f73fadd32ff55c67fbbb58d21a0b0120b63eaa4 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 25 Mar 2019 11:47:38 +0100 Subject: [PATCH 01/14] Export: syntax and parsing --- compiler/src/dotty/tools/dotc/ast/Trees.scala | 2 +- compiler/src/dotty/tools/dotc/ast/untpd.scala | 11 +++++ .../dotty/tools/dotc/parsing/Parsers.scala | 46 +++++++++++-------- .../src/dotty/tools/dotc/parsing/Tokens.scala | 5 +- docs/docs/internals/syntax.md | 4 +- 5 files changed, 46 insertions(+), 22 deletions(-) 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..5f541aafac7d 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(impliedOnly, 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,7 @@ object Parsers { else stats += packaging(start) } else if (in.token == IMPORT) - stats ++= importClause() + stats ++= importClause(IMPORT, Import) else if (in.token == AT || isDefIntro(modifierTokens)) stats +++= defOrDcl(in.offset, defAnnotsMods(modifierTokens)) else if (!isStatSep) { @@ -2816,7 +2824,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 +2898,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/docs/docs/internals/syntax.md b/docs/docs/internals/syntax.md index 74a9d33715d1..12100358332c 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] From 426bbf71b9d31cf5756b2777337e563a1eacdf1c Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 25 Mar 2019 18:09:15 +0100 Subject: [PATCH 02/14] Export: Type checking --- .../dotty/tools/dotc/parsing/Parsers.scala | 2 +- .../src/dotty/tools/dotc/typer/Checking.scala | 6 ++ .../src/dotty/tools/dotc/typer/Namer.scala | 102 ++++++++++++++++++ .../src/dotty/tools/dotc/typer/Typer.scala | 6 +- 4 files changed, 113 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 5f541aafac7d..39859eb2908b 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2256,7 +2256,7 @@ object Parsers { } def derived(impExp: Tree, qual: Tree, selectors: List[Tree]) = - mkTree(impliedOnly, qual, selectors).withSpan(impExp.span) + mkTree(importImplied, qual, selectors).withSpan(impExp.span) () => { val p = path(thisOK = false, handleImport) 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..f8fc3bd2fc92 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,104 @@ 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 = { + + /** 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 needsForwarder(sym: Symbol) = + sym.is(ImplicitOrImplied) == exp.impliedOnly && + sym.isAccessibleFrom(path.tpe) && + !sym.isConstructor && + !cls.derivesFrom(sym.owner) + + /** 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 (needsForwarder(mbr.symbol)) { + + /** 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 => + 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 + ctx.newSymbol( + cls, alias, + Method | Final | mbr.symbol.flags & ImplicitOrImplied, + mbr.info, + 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 mbrs = List(name, name.toTypeName).flatMap(path.tpe.member(_).alternatives) + if (mbrs.isEmpty) + ctx.error(i"no accessible member $name at $path", ctx.source.atSpan(span)) + mbrs.foreach(addForwarder(alias, _, 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 @@ -1068,12 +1167,15 @@ class Namer { typer: Typer => tempInfo.finalize(denot, parentTypes, finalSelfInfo) + + Checking.checkWellFormed(cls) if (isDerivedValueClass(cls)) cls.setFlag(Final) cls.info = avoidPrivateLeaks(cls, cls.sourcePos) 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..2d4c58295ac6 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,9 @@ class Typer extends Namer } case Thicket(stats) :: rest => traverse(stats ++ rest) + case (stat: untpd.Export) :: rest => + buf ++= stat.attachment(ExportForwarders) + traverse(rest) case stat :: rest => val stat1 = typed(stat)(ctx.exprContext(stat, exprOwner)) checkStatementPurity(stat1)(stat, exprOwner) From aabf4a282953b484dd4a8fd1e88305e92df36f6b Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 25 Mar 2019 18:13:10 +0100 Subject: [PATCH 03/14] Test --- tests/run/exports.scala | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/run/exports.scala diff --git a/tests/run/exports.scala b/tests/run/exports.scala new file mode 100644 index 000000000000..3a729158d965 --- /dev/null +++ b/tests/run/exports.scala @@ -0,0 +1,28 @@ +object Test extends App { + + case class Config() + + class Printer { + def print() = println("printing") + 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.config + Copier.config2 +} \ No newline at end of file From 9eb006a2c3533791749ccd2e18d9d957d58d2799 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 25 Mar 2019 18:44:06 +0100 Subject: [PATCH 04/14] Bug fixes --- compiler/src/dotty/tools/dotc/typer/Namer.scala | 3 ++- tests/run/exports.check | 4 ++++ tests/run/exports.scala | 8 ++++++-- 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 tests/run/exports.check diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index f8fc3bd2fc92..190a1b9c709c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -948,6 +948,7 @@ class Namer { typer: Typer => sym.is(ImplicitOrImplied) == exp.impliedOnly && sym.isAccessibleFrom(path.tpe) && !sym.isConstructor && + !sym.is(ModuleClass) && !cls.derivesFrom(sym.owner) /** Add a forwarder with name `alias` or its type name equivalent to `mbr`, @@ -981,7 +982,7 @@ class Namer { typer: Typer => ctx.newSymbol( cls, alias, Method | Final | mbr.symbol.flags & ImplicitOrImplied, - mbr.info, + mbr.info.ensureMethodic, coord = span) val forwarderDef = if (forwarder.isType) tpd.TypeDef(forwarder.asType) 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 index 3a729158d965..5193278e5e0c 100644 --- a/tests/run/exports.scala +++ b/tests/run/exports.scala @@ -1,10 +1,13 @@ object Test extends App { - case class Config() + case class Config() { + println("config") + } class Printer { def print() = println("printing") - implied config for Config() + object cfg extends Config + implied config for Config } class Scanner { @@ -23,6 +26,7 @@ object Test extends App { Copier.print() Copier.scanIt() + Copier.cfg Copier.config Copier.config2 } \ No newline at end of file From aeff1999f115cca18f6f5ba5e02ff370d8e8ed13 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 26 Mar 2019 11:28:28 +0100 Subject: [PATCH 05/14] Allow exports as toplevel statements --- compiler/src/dotty/tools/dotc/ast/Desugar.scala | 2 +- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 2 ++ docs/docs/internals/syntax.md | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) 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/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 39859eb2908b..d74bbfff8ab0 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2778,6 +2778,8 @@ object Parsers { } else if (in.token == IMPORT) 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) { diff --git a/docs/docs/internals/syntax.md b/docs/docs/internals/syntax.md index 12100358332c..bd0eada17306 100644 --- a/docs/docs/internals/syntax.md +++ b/docs/docs/internals/syntax.md @@ -408,6 +408,7 @@ EnumCase ::= ‘case’ (id ClassConstr [‘extends’ ConstrApps]] | TopStatSeq ::= TopStat {semi TopStat} TopStat ::= Import + | Export | {Annotation [nl]} {Modifier} Def | Packaging | PackageObject From a40ad346939ddc2198c647f9fdf84d3001d04551 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 26 Mar 2019 11:28:55 +0100 Subject: [PATCH 06/14] Make export aliases of stable values stable --- compiler/src/dotty/tools/dotc/typer/Namer.scala | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 190a1b9c709c..b66c9d2c50c9 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -949,6 +949,7 @@ class Namer { typer: Typer => sym.isAccessibleFrom(path.tpe) && !sym.isConstructor && !sym.is(ModuleClass) && + !sym.is(Bridge) && !cls.derivesFrom(sym.owner) /** Add a forwarder with name `alias` or its type name equivalent to `mbr`, @@ -978,12 +979,14 @@ class Namer { typer: Typer => Final, fwdInfo(path.tpe.select(mbr.symbol), mbr.info), coord = span) - else + else { + val maybeStable = if (mbr.symbol.isStableMember) StableRealizable else EmptyFlags ctx.newSymbol( cls, alias, - Method | Final | mbr.symbol.flags & ImplicitOrImplied, + Method | Final | maybeStable | mbr.symbol.flags & ImplicitOrImplied, mbr.info.ensureMethodic, coord = span) + } val forwarderDef = if (forwarder.isType) tpd.TypeDef(forwarder.asType) else { @@ -1168,8 +1171,6 @@ class Namer { typer: Typer => tempInfo.finalize(denot, parentTypes, finalSelfInfo) - - Checking.checkWellFormed(cls) if (isDerivedValueClass(cls)) cls.setFlag(Final) cls.info = avoidPrivateLeaks(cls, cls.sourcePos) From b325f93fd1818127a263c8fa9542929674837786 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 26 Mar 2019 11:29:14 +0100 Subject: [PATCH 07/14] Docs and tests --- .../reference/other-new-features/export.md | 144 ++++++++++++++++++ tests/pos/reference/exports.scala | 29 ++++ 2 files changed, 173 insertions(+) create mode 100644 docs/docs/reference/other-new-features/export.md create mode 100644 tests/pos/reference/exports.scala 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..9f33fb66b8f8 --- /dev/null +++ b/docs/docs/reference/other-new-features/export.md @@ -0,0 +1,144 @@ +--- +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 three 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 => _` presents `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`. + +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 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 object. diff --git a/tests/pos/reference/exports.scala b/tests/pos/reference/exports.scala new file mode 100644 index 000000000000..4e191050a0c1 --- /dev/null +++ b/tests/pos/reference/exports.scala @@ -0,0 +1,29 @@ + 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 C { type T } + object O { val c: C = ??? } + export O.c + def f: c.T = ??? + From 4bb7a6b632700a0ac228d0427ec828851eaf3c24 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 26 Mar 2019 11:43:51 +0100 Subject: [PATCH 08/14] Doc tweaks --- docs/docs/reference/other-new-features/export.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/docs/reference/other-new-features/export.md b/docs/docs/reference/other-new-features/export.md index 9f33fb66b8f8..b090fd5abe2d 100644 --- a/docs/docs/reference/other-new-features/export.md +++ b/docs/docs/reference/other-new-features/export.md @@ -47,11 +47,11 @@ An export clause has the same format as an import clause. Its general form is: ``` 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 three forms: +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 => _` presents `x` from being aliased by a subsequent + - 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. @@ -64,6 +64,9 @@ A member is _eligible_ if all of the following holds: - 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: @@ -141,4 +144,4 @@ With export clauses, the following steps are added: 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 object. +by another export clause of the same class. From 37aa7c01470dc1c11eb63c6c71939a7a4fd42b13 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 26 Mar 2019 13:19:08 +0100 Subject: [PATCH 09/14] Rename classes to avoid 'compiled twice' errors --- tests/pos/reference/exports.scala | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/pos/reference/exports.scala b/tests/pos/reference/exports.scala index 4e191050a0c1..fe924c57e489 100644 --- a/tests/pos/reference/exports.scala +++ b/tests/pos/reference/exports.scala @@ -22,8 +22,7 @@ def status: List[String] = printUnit.status ++ scanUnit.status } - class C { type T } - object O { val c: C = ??? } - export O.c - def f: c.T = ??? - + class C22 { type T } + object O22 { val c: C22 = ??? } + export O22.c + def f22: c.T = ??? From 713468b7f127ab3237a9986587d4ce4c469810cc Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 26 Mar 2019 15:44:26 +0100 Subject: [PATCH 10/14] Better error messages when no export aliases exist --- .../src/dotty/tools/dotc/typer/Namer.scala | 30 +++++++++------ tests/neg/exports.check | 24 ++++++++++++ tests/neg/exports.scala | 37 +++++++++++++++++++ 3 files changed, 80 insertions(+), 11 deletions(-) create mode 100644 tests/neg/exports.check create mode 100644 tests/neg/exports.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index b66c9d2c50c9..10ed442a92b5 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -944,19 +944,20 @@ class Namer { typer: Typer => val path = typedAheadExpr(expr, AnySelectionProto) checkLegalImportPath(path) - def needsForwarder(sym: Symbol) = - sym.is(ImplicitOrImplied) == exp.impliedOnly && - sym.isAccessibleFrom(path.tpe) && - !sym.isConstructor && - !sym.is(ModuleClass) && - !sym.is(Bridge) && - !cls.derivesFrom(sym.owner) + 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)) "_" + else if (cls.derivesFrom(sym.owner)) 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 (needsForwarder(mbr.symbol)) { + def addForwarder(alias: TermName, mbr: SingleDenotation, span: Span): Unit = { + if (whyNoForwarder(mbr) == "") { /** The info of a forwarder to type `ref` which has info `info` */ @@ -998,12 +999,19 @@ class Namer { typer: Typer => } 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) - if (mbrs.isEmpty) - ctx.error(i"no accessible member $name at $path", ctx.source.atSpan(span)) mbrs.foreach(addForwarder(alias, _, span)) + if (buf.size == size) { + val reason = mbrs.map(whyNoForwarder).dropWhile(_ == "-") 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 = diff --git a/tests/neg/exports.check b/tests/neg/exports.check new file mode 100644 index 000000000000..d463cd1c1fe2 --- /dev/null +++ b/tests/neg/exports.check @@ -0,0 +1,24 @@ +[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..dcd8718529ef --- /dev/null +++ b/tests/neg/exports.scala @@ -0,0 +1,37 @@ + 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 + +} \ No newline at end of file From 570ed9fdcb8defc0d2cf662156d387ce2b306188 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 26 Mar 2019 20:59:03 +0100 Subject: [PATCH 11/14] Fix typo --- docs/docs/reference/other-new-features/export.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/reference/other-new-features/export.md b/docs/docs/reference/other-new-features/export.md index b090fd5abe2d..48bc2a43ab3a 100644 --- a/docs/docs/reference/other-new-features/export.md +++ b/docs/docs/reference/other-new-features/export.md @@ -76,7 +76,7 @@ Export aliases are always `final`. Aliases of implied instances are again `impli - 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 they can be used as parts of stable identifier paths, even though they are technically methods. For instance, the following is OK: +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 = ... } From 87eff21bcefa801e358ab59912ec8f9f00724580 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Wed, 27 Mar 2019 09:20:46 +0100 Subject: [PATCH 12/14] Allow implementing deferred members by export aliases So far, this gave a "member already exists" error. Also, fix info of type export aliases. --- compiler/src/dotty/tools/dotc/typer/Namer.scala | 5 +++-- tests/pos/export-proxies.scala | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 tests/pos/export-proxies.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 10ed442a92b5..32277c8b8807 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -949,7 +949,8 @@ class Namer { typer: Typer => 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)) "_" - else if (cls.derivesFrom(sym.owner)) i"is already a member of $cls" + else if (cls.derivesFrom(sym.owner) && + (sym.owner == cls || !sym.is(Deferred))) i"is already a member of $cls" else "" } @@ -965,7 +966,7 @@ class Namer { typer: Typer => case _: ClassInfo => HKTypeLambda.fromParams(info.typeParams, ref) case _: TypeBounds => - ref + TypeAlias(ref) case info: HKTypeLambda => info.derivedLambdaType(info.paramNames, info.paramInfos, fwdInfo(ref.appliedTo(info.paramRefs), info.resultType)) 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 From 3022cf668e46bbeefd704b5c30a8bdf5a12e4a0f Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 2 Apr 2019 16:42:02 +0200 Subject: [PATCH 13/14] Address review comments - Clearer code for skipped whynoMatchStr - Surivive missing ExportForwarders attachments - Add tests with self-referential exports --- compiler/src/dotty/tools/dotc/typer/Namer.scala | 7 +++++-- compiler/src/dotty/tools/dotc/typer/Typer.scala | 3 ++- tests/neg/exports.scala | 16 +++++++++++++++- tests/run/exports.scala | 5 +++++ 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 32277c8b8807..e3f299d8f833 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -936,6 +936,9 @@ class Namer { typer: Typer => /** 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] = { @@ -948,7 +951,7 @@ class Namer { typer: Typer => 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)) "_" + 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 "" @@ -1007,7 +1010,7 @@ class Namer { typer: Typer => 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(_ == "-") match { + val reason = mbrs.map(whyNoForwarder).dropWhile(_ == SKIP) match { case Nil => "" case why :: _ => i"\n$path.$name cannot be exported because it $why" } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 2d4c58295ac6..4edc94308703 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2205,7 +2205,8 @@ class Typer extends Namer case Thicket(stats) :: rest => traverse(stats ++ rest) case (stat: untpd.Export) :: rest => - buf ++= stat.attachment(ExportForwarders) + 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)) diff --git a/tests/neg/exports.scala b/tests/neg/exports.scala index dcd8718529ef..2f5f583d39ff 100644 --- a/tests/neg/exports.scala +++ b/tests/neg/exports.scala @@ -34,4 +34,18 @@ trait IterableOps[+A, +CC[_], +C] { export this.{concat => ++} // error: no eligible member -} \ No newline at end of file +} + +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/run/exports.scala b/tests/run/exports.scala index 5193278e5e0c..489abe281dfb 100644 --- a/tests/run/exports.scala +++ b/tests/run/exports.scala @@ -29,4 +29,9 @@ object Test extends App { 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 From 1731ba770156c0ba6e1128f2e9cded9aa224f472 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Wed, 3 Apr 2019 11:44:24 +0200 Subject: [PATCH 14/14] Update checkfile --- tests/neg/exports.check | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/neg/exports.check b/tests/neg/exports.check index d463cd1c1fe2..b5fd10abb817 100644 --- a/tests/neg/exports.check +++ b/tests/neg/exports.check @@ -1,3 +1,8 @@ +<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