From 7c73e5444665a0f7ec0125d987728e5090141ff1 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 1 Dec 2019 15:15:10 +0100 Subject: [PATCH 01/16] Fix #7650: Drop stale toplevel definitions --- .../dotty/tools/dotc/core/Denotations.scala | 6 +++++ .../tools/dotc/core/SymDenotations.scala | 23 +++++++++++++++---- tests/pos/i7650/Test_1.scala | 1 + tests/pos/i7650/Test_2.scala | 1 + 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 tests/pos/i7650/Test_1.scala create mode 100644 tests/pos/i7650/Test_2.scala diff --git a/compiler/src/dotty/tools/dotc/core/Denotations.scala b/compiler/src/dotty/tools/dotc/core/Denotations.scala index e47def78fba1..1b5096640b2b 100644 --- a/compiler/src/dotty/tools/dotc/core/Denotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Denotations.scala @@ -116,6 +116,9 @@ object Denotations { */ def filterWithFlags(required: FlagSet, excluded: FlagSet)(implicit ctx: Context): PreDenotation + /** Map `f` over all single denotations and aggregate the results with `g`. */ + def aggregate[T](f: SingleDenotation => T, g: (T, T) => T): T + private var cachedPrefix: Type = _ private var cachedAsSeenFrom: AsSeenFromResult = _ private var validAsSeenFrom: Period = Nowhere @@ -1132,6 +1135,7 @@ object Denotations { if (denots.exists && denots.matches(this)) NoDenotation else this def filterWithFlags(required: FlagSet, excluded: FlagSet)(implicit ctx: Context): SingleDenotation = if (required.isEmpty && excluded.isEmpty || compatibleWith(required, excluded)) this else NoDenotation + def aggregate[T](f: SingleDenotation => T, g: (T, T) => T): T = f(this) type AsSeenFromResult = SingleDenotation protected def computeAsSeenFrom(pre: Type)(implicit ctx: Context): SingleDenotation = { @@ -1286,6 +1290,8 @@ object Denotations { derivedUnion(denot1 filterDisjoint denot, denot2 filterDisjoint denot) def filterWithFlags(required: FlagSet, excluded: FlagSet)(implicit ctx: Context): PreDenotation = derivedUnion(denot1.filterWithFlags(required, excluded), denot2.filterWithFlags(required, excluded)) + def aggregate[T](f: SingleDenotation => T, g: (T, T) => T): T = + g(denot1.aggregate(f, g), denot2.aggregate(f, g)) protected def derivedUnion(denot1: PreDenotation, denot2: PreDenotation) = if ((denot1 eq this.denot1) && (denot2 eq this.denot2)) this else denot1 union denot2 diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 50b940ec9211..9028db6b437b 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -2122,9 +2122,9 @@ object SymDenotations { case pcls :: pobjs1 => if (pcls.isCompleting) recur(pobjs1, acc) else { - // A package object inherits members from `Any` and `Object` which - // should not be accessible from the package prefix. val pmembers = pcls.computeNPMembersNamed(name).filterWithPredicate { d => + // Drop members of `Any` and `Object`, as well as top-level definitions + // in the empty package that are not defined in the current run. val owner = d.symbol.maybeOwner (owner ne defn.AnyClass) && (owner ne defn.ObjectClass) } @@ -2132,9 +2132,24 @@ object SymDenotations { } case nil => val directMembers = super.computeNPMembersNamed(name) - if (acc.exists) acc.union(directMembers.filterWithPredicate(!_.symbol.isAbsent())) - else directMembers + if !acc.exists then directMembers + else acc.union(directMembers.filterWithPredicate(!_.symbol.isAbsent())) match + case d: DenotUnion => dropStale(d) + case d => d } + + def dropStale(d: DenotUnion): PreDenotation = + val compiledNow = d.filterWithPredicate(d => + d.symbol.isDefinedInCurrentRun || d.symbol.associatedFile == null + // if a symbol does not have an associated file, assume it is defined + // in the current run anyway. This typically happens for pickling and + // from-tasty tests that generate a fresh symbol and then re-use it in the next run. + ) + if compiledNow.exists then compiledNow + else + val youngest = d.aggregate(_.symbol.associatedFile.lastModified, _ max _) + d.filterWithPredicate(_.symbol.associatedFile.lastModified == youngest) + if (symbol `eq` defn.ScalaPackageClass) { val denots = super.computeNPMembersNamed(name) if (denots.exists) denots diff --git a/tests/pos/i7650/Test_1.scala b/tests/pos/i7650/Test_1.scala new file mode 100644 index 000000000000..01bc62e8e357 --- /dev/null +++ b/tests/pos/i7650/Test_1.scala @@ -0,0 +1 @@ +@main def Test() = println("hi") \ No newline at end of file diff --git a/tests/pos/i7650/Test_2.scala b/tests/pos/i7650/Test_2.scala new file mode 100644 index 000000000000..00174769a0f0 --- /dev/null +++ b/tests/pos/i7650/Test_2.scala @@ -0,0 +1 @@ +object Test From 74aabd6a81b95a0c72e75a478ebda3e1fec2b50a Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 1 Dec 2019 15:26:11 +0100 Subject: [PATCH 02/16] Test case for stale overloading Show that toplevel definitions on several files do not combine as overloaded denotations. Instead, the youngest definition wins. --- tests/run/toplevel-stale/A_1.scala | 1 + tests/run/toplevel-stale/B_2.scala | 1 + tests/run/toplevel-stale/Test_3.scala | 2 ++ 3 files changed, 4 insertions(+) create mode 100644 tests/run/toplevel-stale/A_1.scala create mode 100644 tests/run/toplevel-stale/B_2.scala create mode 100644 tests/run/toplevel-stale/Test_3.scala diff --git a/tests/run/toplevel-stale/A_1.scala b/tests/run/toplevel-stale/A_1.scala new file mode 100644 index 000000000000..4026bf0b4bb0 --- /dev/null +++ b/tests/run/toplevel-stale/A_1.scala @@ -0,0 +1 @@ +def foo(x: Int): String = "old" \ No newline at end of file diff --git a/tests/run/toplevel-stale/B_2.scala b/tests/run/toplevel-stale/B_2.scala new file mode 100644 index 000000000000..d1ac08dd50d5 --- /dev/null +++ b/tests/run/toplevel-stale/B_2.scala @@ -0,0 +1 @@ +def foo(x: Long): String = "new" \ No newline at end of file diff --git a/tests/run/toplevel-stale/Test_3.scala b/tests/run/toplevel-stale/Test_3.scala new file mode 100644 index 000000000000..7dbaf3857e91 --- /dev/null +++ b/tests/run/toplevel-stale/Test_3.scala @@ -0,0 +1,2 @@ +@main def Test() = + assert(foo(1) == "new") From 25433d19ea7b8ba2bd75cbf45b5ceab375b0d638 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 1 Dec 2019 15:34:08 +0100 Subject: [PATCH 03/16] Update doc page --- docs/docs/reference/dropped-features/package-objects.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/docs/reference/dropped-features/package-objects.md b/docs/docs/reference/dropped-features/package-objects.md index ee1b1a5d6f24..50c273d4e817 100644 --- a/docs/docs/reference/dropped-features/package-objects.md +++ b/docs/docs/reference/dropped-features/package-objects.md @@ -42,3 +42,6 @@ in a source file `src.scala`, it could be invoked from the command line using a "program name" is mangled it is recommended to always put `main` methods in explicitly named objects. **Note 3:** The notion of `private` is independent of whether a definition is wrapped or not. A `private` toplevel definition is always visible from everywhere in the enclosing package. + +**Note 4:** If several wrapper objects contain toplevel definitions with the same name, +only the definition that is compiled last is retained. As a consequence, it is impossible to have overloaded toplevel definitions in different files, as the alternatives in the last-compiled file always replace the alternatives compiled earlier. From e055f5b72ac571a7b04906876b8f8fc97b112686 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 1 Dec 2019 19:42:10 +0100 Subject: [PATCH 04/16] Fix neg test --- tests/run/toplevel-mixed/B_1.scala | 2 +- tests/run/toplevel-mixed/Test_2.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/run/toplevel-mixed/B_1.scala b/tests/run/toplevel-mixed/B_1.scala index a27e0c8fde37..f89f6e279d56 100644 --- a/tests/run/toplevel-mixed/B_1.scala +++ b/tests/run/toplevel-mixed/B_1.scala @@ -1,7 +1,7 @@ class x(val s: String) -object y { +object yy { def foo = 3 } diff --git a/tests/run/toplevel-mixed/Test_2.scala b/tests/run/toplevel-mixed/Test_2.scala index 01219321807b..45075ef16a7b 100644 --- a/tests/run/toplevel-mixed/Test_2.scala +++ b/tests/run/toplevel-mixed/Test_2.scala @@ -4,7 +4,7 @@ object Test extends App { assert((new x("abc")).s == "abc") assert(y("abc") == 3) - assert(y.foo == 3) + assert(yy.foo == 3) val x2: X2 = "abc" assert(X2.bar == "hi") From 1cdcc3647681cc422e84f2a25d498da511b8ac5e Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 2 Dec 2019 08:46:30 +0100 Subject: [PATCH 05/16] Drop flaky tests Toplevel overloading tests are no longer supported. The toplevel doubledef test works only if two generated class files have the same modification date, which changes from run to run. --- tests/neg/toplevel-doubledef/Test_2.scala | 3 --- tests/neg/toplevel-doubledef/defs_1.scala | 2 -- tests/neg/toplevel-doubledef/moredefs_1.scala | 2 -- tests/neg/toplevel-overload/Test_3.scala | 6 ------ tests/neg/toplevel-overload/defs_1.scala | 3 --- tests/neg/toplevel-overload/defs_2.scala | 3 --- tests/run/toplevel-overloads.check | 4 ---- tests/run/toplevel-overloads/Test_2.scala | 7 ------- tests/run/toplevel-overloads/defs_1.scala | 12 ------------ tests/run/toplevel-overloads/moredefs_1.scala | 4 ---- 10 files changed, 46 deletions(-) delete mode 100644 tests/neg/toplevel-doubledef/Test_2.scala delete mode 100644 tests/neg/toplevel-doubledef/defs_1.scala delete mode 100644 tests/neg/toplevel-doubledef/moredefs_1.scala delete mode 100644 tests/neg/toplevel-overload/Test_3.scala delete mode 100644 tests/neg/toplevel-overload/defs_1.scala delete mode 100644 tests/neg/toplevel-overload/defs_2.scala delete mode 100644 tests/run/toplevel-overloads.check delete mode 100644 tests/run/toplevel-overloads/Test_2.scala delete mode 100644 tests/run/toplevel-overloads/defs_1.scala delete mode 100644 tests/run/toplevel-overloads/moredefs_1.scala diff --git a/tests/neg/toplevel-doubledef/Test_2.scala b/tests/neg/toplevel-doubledef/Test_2.scala deleted file mode 100644 index 0539a4be38d0..000000000000 --- a/tests/neg/toplevel-doubledef/Test_2.scala +++ /dev/null @@ -1,3 +0,0 @@ -object Test extends App{ - hello("hello") // error: Cannot merge -} \ No newline at end of file diff --git a/tests/neg/toplevel-doubledef/defs_1.scala b/tests/neg/toplevel-doubledef/defs_1.scala deleted file mode 100644 index f25b23ba17c4..000000000000 --- a/tests/neg/toplevel-doubledef/defs_1.scala +++ /dev/null @@ -1,2 +0,0 @@ -def hello(x: String): Unit = - println(s"hello: $x") \ No newline at end of file diff --git a/tests/neg/toplevel-doubledef/moredefs_1.scala b/tests/neg/toplevel-doubledef/moredefs_1.scala deleted file mode 100644 index 6b5c7d12a938..000000000000 --- a/tests/neg/toplevel-doubledef/moredefs_1.scala +++ /dev/null @@ -1,2 +0,0 @@ -def hello(x: String): Unit = - println(s"hi, $x") \ No newline at end of file diff --git a/tests/neg/toplevel-overload/Test_3.scala b/tests/neg/toplevel-overload/Test_3.scala deleted file mode 100644 index 1d1dd246340e..000000000000 --- a/tests/neg/toplevel-overload/Test_3.scala +++ /dev/null @@ -1,6 +0,0 @@ -class C extends A, B - -val c = new C - -val d = f(c) // error: ambiguous overload - diff --git a/tests/neg/toplevel-overload/defs_1.scala b/tests/neg/toplevel-overload/defs_1.scala deleted file mode 100644 index 5c74aaa1249b..000000000000 --- a/tests/neg/toplevel-overload/defs_1.scala +++ /dev/null @@ -1,3 +0,0 @@ -trait A - -def f(x: A) = s"A" diff --git a/tests/neg/toplevel-overload/defs_2.scala b/tests/neg/toplevel-overload/defs_2.scala deleted file mode 100644 index f2ac0f806854..000000000000 --- a/tests/neg/toplevel-overload/defs_2.scala +++ /dev/null @@ -1,3 +0,0 @@ -trait B - -def f(x: B) = s"B" diff --git a/tests/run/toplevel-overloads.check b/tests/run/toplevel-overloads.check deleted file mode 100644 index cb9ff070ab31..000000000000 --- a/tests/run/toplevel-overloads.check +++ /dev/null @@ -1,4 +0,0 @@ -hello, Bill -3 -yes -hello, Bob diff --git a/tests/run/toplevel-overloads/Test_2.scala b/tests/run/toplevel-overloads/Test_2.scala deleted file mode 100644 index 8a1beba2b2ff..000000000000 --- a/tests/run/toplevel-overloads/Test_2.scala +++ /dev/null @@ -1,7 +0,0 @@ -import top._ -object Test extends App { - println(hello("Bill")) - println(hello(3)) - println(hello(true)) - println(O.hi) -} \ No newline at end of file diff --git a/tests/run/toplevel-overloads/defs_1.scala b/tests/run/toplevel-overloads/defs_1.scala deleted file mode 100644 index 2efcc72c6475..000000000000 --- a/tests/run/toplevel-overloads/defs_1.scala +++ /dev/null @@ -1,12 +0,0 @@ -package top - -def hello(name: String) = s"hello, $name" -def hello(x: Int) = x.toString - -object O { - def hi = hello("Bob") - def gb = hello(true) -} - -val test1 = top.hello(false) -val test2 = hello(false) diff --git a/tests/run/toplevel-overloads/moredefs_1.scala b/tests/run/toplevel-overloads/moredefs_1.scala deleted file mode 100644 index 78e5b21b0240..000000000000 --- a/tests/run/toplevel-overloads/moredefs_1.scala +++ /dev/null @@ -1,4 +0,0 @@ -package top - -def hello(b: Boolean): String = if (b) "yes" else "no" - From 7fc54b94962c639aa6c2e8bedbba01e2c036a3b0 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 2 Dec 2019 18:27:10 +0100 Subject: [PATCH 06/16] Drop stray comment --- compiler/src/dotty/tools/dotc/core/SymDenotations.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 9028db6b437b..6c8e6c1587da 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -2123,8 +2123,6 @@ object SymDenotations { if (pcls.isCompleting) recur(pobjs1, acc) else { val pmembers = pcls.computeNPMembersNamed(name).filterWithPredicate { d => - // Drop members of `Any` and `Object`, as well as top-level definitions - // in the empty package that are not defined in the current run. val owner = d.symbol.maybeOwner (owner ne defn.AnyClass) && (owner ne defn.ObjectClass) } From 13e1b780e7c0b77b6425e86ef7cfa4b50833e36a Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 5 Dec 2019 11:35:56 +0100 Subject: [PATCH 07/16] Invalidate package's member cache when entering a toplevel definition Invalidate enclosing package's member cache when entering a toplevel definition in a wrapper package object. --- .../tools/dotc/core/SymDenotations.scala | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 6c8e6c1587da..9c1c334a1d15 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -625,6 +625,10 @@ object SymDenotations { def isPackageObject(implicit ctx: Context): Boolean = name.isPackageObjectName && owner.is(Package) && this.is(Module) + /** Is this symbol a toplevel definition in a package object? */ + def isWrappedToplevelDef(given Context): Boolean = + !isConstructor && owner.isPackageObject + /** Is this symbol an abstract type? */ final def isAbstractType(implicit ctx: Context): Boolean = this.is(DeferredType) @@ -1527,6 +1531,14 @@ object SymDenotations { myBaseTypeCachePeriod = Nowhere } + def invalidateMemberCaches(sym: Symbol)(given Context): Unit = + if myMemberCache != null then myMemberCache.invalidate(sym.name) + if !sym.flagsUNSAFE.is(Private) then + invalidateMemberNamesCache() + if sym.isWrappedToplevelDef then + val outerCache = sym.owner.owner.asClass.classDenot.myMemberCache + if outerCache != null then outerCache.invalidate(sym.name) + override def copyCaches(from: SymDenotation, phase: Phase)(implicit ctx: Context): this.type = { from match { case from: ClassDenotation => @@ -1726,11 +1738,9 @@ object SymDenotations { } /** Enter a symbol in given `scope` without potentially replacing the old copy. */ - def enterNoReplace(sym: Symbol, scope: MutableScope)(implicit ctx: Context): Unit = { + def enterNoReplace(sym: Symbol, scope: MutableScope)(given Context): Unit = scope.enter(sym) - if (myMemberCache != null) myMemberCache.invalidate(sym.name) - if (!sym.flagsUNSAFE.is(Private)) invalidateMemberNamesCache() - } + invalidateMemberCaches(sym) /** Replace symbol `prev` (if defined in current class) by symbol `replacement`. * If `prev` is not defined in current class, do nothing. @@ -2123,6 +2133,7 @@ object SymDenotations { if (pcls.isCompleting) recur(pobjs1, acc) else { val pmembers = pcls.computeNPMembersNamed(name).filterWithPredicate { d => + // Drop members of `Any` and `Object` val owner = d.symbol.maybeOwner (owner ne defn.AnyClass) && (owner ne defn.ObjectClass) } @@ -2140,7 +2151,7 @@ object SymDenotations { val compiledNow = d.filterWithPredicate(d => d.symbol.isDefinedInCurrentRun || d.symbol.associatedFile == null // if a symbol does not have an associated file, assume it is defined - // in the current run anyway. This typically happens for pickling and + // in the current run anyway. This is true for packages, and also can happen for pickling and // from-tasty tests that generate a fresh symbol and then re-use it in the next run. ) if compiledNow.exists then compiledNow From e5b30237f8a27f63aaa5ff247ed1b1fee71f864b Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 6 Dec 2019 16:40:39 +0100 Subject: [PATCH 08/16] Better error message when package names conflict with other defs --- .../dotty/tools/dotc/reporting/diagnostic/messages.scala | 9 +++++++-- compiler/src/dotty/tools/dotc/typer/Typer.scala | 7 ++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala b/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala index 1c5c1941ab25..9a64593c614f 100644 --- a/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala @@ -1908,10 +1908,15 @@ object messages { } case class PackageNameAlreadyDefined(pkg: Symbol)(implicit ctx: Context) extends Message(PackageNameAlreadyDefinedID) { - val msg: String = em"${pkg} is already defined, cannot be a ${hl("package")}" + val (where, or) = + if pkg.associatedFile == null then ("", "") + else (s" in ${pkg.associatedFile}", " or delete the containing class file") + val msg: String = em"""${pkg.name} is the name of $pkg$where. + |It cannot be used at the same time as the name of a package.""" val kind: String = "Naming" val explanation: String = - em"An ${hl("object")} cannot have the same name as an existing ${hl("package")}. Rename either one of them." + em"""An ${hl("object")} or other toplevel definition cannot have the same name as an existing ${hl("package")}. + |Rename either one of them$or.""" } case class UnapplyInvalidNumberOfArguments(qual: untpd.Tree, argTypes: List[Type])(implicit ctx: Context) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index b8e262d2d239..cb7973f088e5 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1879,8 +1879,7 @@ class Typer extends Namer val pid1 = typedExpr(tree.pid, AnySelectionProto)(ctx.addMode(Mode.InPackageClauseName)) val pkg = pid1.symbol pid1 match { - case pid1: RefTree if pkg.exists => - if (!pkg.is(Package)) ctx.error(PackageNameAlreadyDefined(pkg), tree.sourcePos) + case pid1: RefTree if pkg.is(Package) => val packageCtx = ctx.packageContext(tree, pkg) var stats1 = typedStats(tree.stats, pkg.moduleClass)(packageCtx)._1 if (!ctx.isAfterTyper) @@ -1888,7 +1887,9 @@ class Typer extends Namer cpy.PackageDef(tree)(pid1, stats1).withType(pkg.termRef) case _ => // Package will not exist if a duplicate type has already been entered, see `tests/neg/1708.scala` - errorTree(tree, i"package ${tree.pid.name} does not exist") + errorTree(tree, + if pkg.exists then PackageNameAlreadyDefined(pkg) + else i"package ${tree.pid.name} does not exist") } } From 2e336b0e41148ae56ca1e8aa3737448d6d53c057 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 7 Dec 2019 21:42:31 +0100 Subject: [PATCH 09/16] Fix cache invalidation of packageObjs --- compiler/src/dotty/tools/dotc/core/SymDenotations.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 9c1c334a1d15..4b54a6fb53bb 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -2178,8 +2178,8 @@ object SymDenotations { recur(packageObjs, super.memberNames(keepOnly)) } - /** If another symbol with the same name is entered, unlink it, - * and, if symbol is a package object, invalidate the packageObj cache. + /** If another symbol with the same name is entered, unlink it. + * If symbol is a package object, invalidate the packageObj cache. * @return `sym` is not already entered */ override def proceedWithEnter(sym: Symbol, mscope: MutableScope)(implicit ctx: Context): Boolean = { @@ -2187,8 +2187,8 @@ object SymDenotations { if (entry != null) { if (entry.sym == sym) return false mscope.unlink(entry) - if (sym.name.isPackageObjectName) packageObjsRunId = NoRunId } + if (sym.name.isPackageObjectName) packageObjsRunId = NoRunId true } From 3a84f47e2f817ee5ce9bd4b929007d3d6b8acb93 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 6 Dec 2019 17:53:47 +0100 Subject: [PATCH 10/16] Detect double toplevel definitions in separate files Flag as error if two separate files contain toplevel definitions with the same name. --- .../src/dotty/tools/dotc/typer/Namer.scala | 54 +++++++++++++------ .../src/dotty/tools/dotc/typer/Typer.scala | 7 ++- tests/neg/toplevel-doubledef/defs_1.scala | 2 + tests/neg/toplevel-doubledef/moredefs_1.scala | 2 + tests/neg/toplevel-overload/defs_1.scala | 5 ++ tests/neg/toplevel-overload/moredefs_1.scala | 5 ++ tests/neg/toplevel-package/defs_1.scala | 6 +++ tests/neg/toplevel-package/moredefs_2.scala | 2 + tests/run/toplevel-mixed/B_1.scala | 2 +- 9 files changed, 65 insertions(+), 20 deletions(-) create mode 100644 tests/neg/toplevel-doubledef/defs_1.scala create mode 100644 tests/neg/toplevel-doubledef/moredefs_1.scala create mode 100644 tests/neg/toplevel-overload/defs_1.scala create mode 100644 tests/neg/toplevel-overload/moredefs_1.scala create mode 100644 tests/neg/toplevel-package/defs_1.scala create mode 100644 tests/neg/toplevel-package/moredefs_2.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index fb70dec6fd04..8217f9866261 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -302,21 +302,45 @@ class Namer { typer: Typer => typr.println(i"creating symbol for $tree in ${ctx.mode}") - def checkNoConflict(name: Name): Name = { - def errorName(msg: => String) = { - ctx.error(msg, tree.sourcePos) - name.freshened - } + def checkNoConflict(name: Name, isPrivate: Boolean): Name = + val owner = ctx.owner + var conflictsDetected = false + + def conflict(conflicting: Symbol) = + val where: String = + if conflicting.owner == owner then "" + else if conflicting.owner.isPackageObject then i" in ${conflicting.associatedFile}" + else i" in ${conflicting.owner}" + ctx.error(i"$name is already defined as $conflicting$where", tree.sourcePos) + conflictsDetected = true + + def checkNoConflictWith(preExisting: Symbol) = + if (preExisting.isDefinedInCurrentRun || preExisting.is(Package)) + && (!preExisting.is(Private) || preExisting.owner.is(Package)) + then conflict(preExisting) + + def checkNoConflictIn(owner: Symbol) = + checkNoConflictWith( owner.unforcedDecls.lookup(name)) + + def pkgObjs(pkg: Symbol) = + pkg.denot.asInstanceOf[PackageClassDenotation].packageObjs.map(_.symbol) + def preExisting = ctx.effectiveScope.lookup(name) - if (ctx.owner.is(PackageClass)) - if (preExisting.isDefinedInCurrentRun) - errorName(s"${preExisting.showLocated} has already been compiled once during this run") - else name + if owner.is(PackageClass) then + checkNoConflictWith(preExisting) + return name + for pkgObj <- pkgObjs(owner) do + checkNoConflictIn(pkgObj) else - if ((!ctx.owner.isClass || name.isTypeName) && preExisting.exists) - errorName(i"$name is already defined as $preExisting") - else name - } + if (!owner.isClass || name.isTypeName) && preExisting.exists then + conflict(preExisting) + else if owner.isPackageObject && !isPrivate && name != nme.CONSTRUCTOR then + checkNoConflictIn(owner.owner) + for pkgObj <- pkgObjs(owner.owner) if pkgObj != owner do + checkNoConflictIn(pkgObj) + + if conflictsDetected then name.freshened else name + end checkNoConflict /** Create new symbol or redefine existing symbol under lateCompile. */ def createOrRefine[S <: Symbol]( @@ -348,8 +372,8 @@ class Namer { typer: Typer => tree match { case tree: TypeDef if tree.isClassDef => - val name = checkNoConflict(tree.name).asTypeName val flags = checkFlags(tree.mods.flags &~ GivenOrImplicit) + val name = checkNoConflict(tree.name, flags.is(Private)).asTypeName val cls = createOrRefine[ClassSymbol](tree, name, flags, ctx.owner, cls => adjustIfModule(new ClassCompleter(cls, tree)(ctx), tree), @@ -357,8 +381,8 @@ class Namer { typer: Typer => cls.completer.asInstanceOf[ClassCompleter].init() cls case tree: MemberDef => - val name = checkNoConflict(tree.name) var flags = checkFlags(tree.mods.flags) + val name = checkNoConflict(tree.name, flags.is(Private)) tree match case tree: ValOrDefDef => if tree.unforcedRhs == EmptyTree diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index cb7973f088e5..c646df6ee279 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1875,10 +1875,10 @@ class Typer extends Namer assignType(cpy.Import(imp)(expr1, selectors1), sym) } - def typedPackageDef(tree: untpd.PackageDef)(implicit ctx: Context): Tree = { + def typedPackageDef(tree: untpd.PackageDef)(implicit ctx: Context): Tree = val pid1 = typedExpr(tree.pid, AnySelectionProto)(ctx.addMode(Mode.InPackageClauseName)) val pkg = pid1.symbol - pid1 match { + pid1 match case pid1: RefTree if pkg.is(Package) => val packageCtx = ctx.packageContext(tree, pkg) var stats1 = typedStats(tree.stats, pkg.moduleClass)(packageCtx)._1 @@ -1890,8 +1890,7 @@ class Typer extends Namer errorTree(tree, if pkg.exists then PackageNameAlreadyDefined(pkg) else i"package ${tree.pid.name} does not exist") - } - } + end typedPackageDef def typedAnnotated(tree: untpd.Annotated, pt: Type)(implicit ctx: Context): Tree = { val annot1 = typedExpr(tree.annot, defn.AnnotationClass.typeRef) diff --git a/tests/neg/toplevel-doubledef/defs_1.scala b/tests/neg/toplevel-doubledef/defs_1.scala new file mode 100644 index 000000000000..f25b23ba17c4 --- /dev/null +++ b/tests/neg/toplevel-doubledef/defs_1.scala @@ -0,0 +1,2 @@ +def hello(x: String): Unit = + println(s"hello: $x") \ No newline at end of file diff --git a/tests/neg/toplevel-doubledef/moredefs_1.scala b/tests/neg/toplevel-doubledef/moredefs_1.scala new file mode 100644 index 000000000000..c131b307ebec --- /dev/null +++ b/tests/neg/toplevel-doubledef/moredefs_1.scala @@ -0,0 +1,2 @@ +def hello(x: String): Unit = // error: has already been compiled + println(s"hi, $x") \ No newline at end of file diff --git a/tests/neg/toplevel-overload/defs_1.scala b/tests/neg/toplevel-overload/defs_1.scala new file mode 100644 index 000000000000..dfbba378d07e --- /dev/null +++ b/tests/neg/toplevel-overload/defs_1.scala @@ -0,0 +1,5 @@ +trait A + +def f(x: A) = s"A" + +def g(): Unit = () \ No newline at end of file diff --git a/tests/neg/toplevel-overload/moredefs_1.scala b/tests/neg/toplevel-overload/moredefs_1.scala new file mode 100644 index 000000000000..5ba8cfc52078 --- /dev/null +++ b/tests/neg/toplevel-overload/moredefs_1.scala @@ -0,0 +1,5 @@ +trait B + +def f(x: B) = s"B" // error: has already been compiled + +private def g(): Unit = () // OK, since it is private \ No newline at end of file diff --git a/tests/neg/toplevel-package/defs_1.scala b/tests/neg/toplevel-package/defs_1.scala new file mode 100644 index 000000000000..9e9af2d3adbf --- /dev/null +++ b/tests/neg/toplevel-package/defs_1.scala @@ -0,0 +1,6 @@ +package foo +package bar + class C + def hello(x: String): Unit = + println(s"hello: $x") + diff --git a/tests/neg/toplevel-package/moredefs_2.scala b/tests/neg/toplevel-package/moredefs_2.scala new file mode 100644 index 000000000000..32e3583b3633 --- /dev/null +++ b/tests/neg/toplevel-package/moredefs_2.scala @@ -0,0 +1,2 @@ +package foo +def bar = 2 // error: bar is a package diff --git a/tests/run/toplevel-mixed/B_1.scala b/tests/run/toplevel-mixed/B_1.scala index f89f6e279d56..de59188c0a83 100644 --- a/tests/run/toplevel-mixed/B_1.scala +++ b/tests/run/toplevel-mixed/B_1.scala @@ -9,5 +9,5 @@ object X2 { def bar = "hi" } -class X3 +class X4 From 2256ca536f806454ac4b9e706a7a2eb533e34104 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 7 Dec 2019 22:04:35 +0100 Subject: [PATCH 11/16] Force less in checkNoConflict --- compiler/src/dotty/tools/dotc/typer/Namer.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 8217f9866261..eb2c154450fd 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -315,8 +315,8 @@ class Namer { typer: Typer => conflictsDetected = true def checkNoConflictWith(preExisting: Symbol) = - if (preExisting.isDefinedInCurrentRun || preExisting.is(Package)) - && (!preExisting.is(Private) || preExisting.owner.is(Package)) + if (preExisting.isDefinedInCurrentRun || preExisting.lastKnownDenotation.is(Package)) + && (!preExisting.lastKnownDenotation.is(Private) || preExisting.owner.is(Package)) then conflict(preExisting) def checkNoConflictIn(owner: Symbol) = From 944facfb19c638d62b5eec040348e5db58c455c6 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 7 Dec 2019 22:43:47 +0100 Subject: [PATCH 12/16] More comments and drop debug statement --- compiler/src/dotty/tools/dotc/typer/Namer.scala | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index eb2c154450fd..49cfdcc2af80 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -302,6 +302,12 @@ class Namer { typer: Typer => typr.println(i"creating symbol for $tree in ${ctx.mode}") + /** Check that a new definition with given name and privacy status + * in current context would not conflict with existing currently + * compiled definitions. + * The logic here is very subtle and fragile due to the fact that + * we are not allowed to force anything. + */ def checkNoConflict(name: Name, isPrivate: Boolean): Name = val owner = ctx.owner var conflictsDetected = false @@ -314,24 +320,21 @@ class Namer { typer: Typer => ctx.error(i"$name is already defined as $conflicting$where", tree.sourcePos) conflictsDetected = true - def checkNoConflictWith(preExisting: Symbol) = + def checkNoConflictIn(owner: Symbol) = + val preExisting = owner.unforcedDecls.lookup(name) if (preExisting.isDefinedInCurrentRun || preExisting.lastKnownDenotation.is(Package)) && (!preExisting.lastKnownDenotation.is(Private) || preExisting.owner.is(Package)) then conflict(preExisting) - def checkNoConflictIn(owner: Symbol) = - checkNoConflictWith( owner.unforcedDecls.lookup(name)) - def pkgObjs(pkg: Symbol) = pkg.denot.asInstanceOf[PackageClassDenotation].packageObjs.map(_.symbol) - def preExisting = ctx.effectiveScope.lookup(name) if owner.is(PackageClass) then - checkNoConflictWith(preExisting) - return name + checkNoConflictIn(owner) for pkgObj <- pkgObjs(owner) do checkNoConflictIn(pkgObj) else + def preExisting = ctx.effectiveScope.lookup(name) if (!owner.isClass || name.isTypeName) && preExisting.exists then conflict(preExisting) else if owner.isPackageObject && !isPrivate && name != nme.CONSTRUCTOR then From 72c0014a7e7fee57f2c7bda7b534cd7f3d515d10 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 8 Dec 2019 11:40:12 +0100 Subject: [PATCH 13/16] Change docs to disallow overloaded toplevels in several source files --- docs/docs/reference/dropped-features/package-objects.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/reference/dropped-features/package-objects.md b/docs/docs/reference/dropped-features/package-objects.md index 50c273d4e817..2e0f51b25fb1 100644 --- a/docs/docs/reference/dropped-features/package-objects.md +++ b/docs/docs/reference/dropped-features/package-objects.md @@ -43,5 +43,5 @@ in a source file `src.scala`, it could be invoked from the command line using a **Note 3:** The notion of `private` is independent of whether a definition is wrapped or not. A `private` toplevel definition is always visible from everywhere in the enclosing package. -**Note 4:** If several wrapper objects contain toplevel definitions with the same name, -only the definition that is compiled last is retained. As a consequence, it is impossible to have overloaded toplevel definitions in different files, as the alternatives in the last-compiled file always replace the alternatives compiled earlier. +**Note 4:** If several toplevel definitions are overloaded variants with the same name, +they must all come from the same source file. From 8ac2c6628228bd9160d5e02d3ba37adc58f53bda Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 8 Dec 2019 12:40:20 +0100 Subject: [PATCH 14/16] Report an error if toplevel denotations come from multiple files Report an error if toplevel denotations the same name with come from multiple files that have the same modification date. The error was reported in some runs for toplevel/stale when run locally since the files for A_1.scala and B_2.scala were generated too fast so they ended up with the same modification date. B_2.scala has now been beefed up to give the compiler more work to do, so that the race is avoided. --- .../tools/dotc/core/SymDenotations.scala | 21 ++++++++-- tests/run/toplevel-stale/A_1.scala | 2 +- tests/run/toplevel-stale/B_2.scala | 39 ++++++++++++++++++- tests/run/toplevel-stale/Test_3.scala | 2 +- 4 files changed, 57 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 4b54a6fb53bb..943e92d77328 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -2147,8 +2147,8 @@ object SymDenotations { case d => d } - def dropStale(d: DenotUnion): PreDenotation = - val compiledNow = d.filterWithPredicate(d => + def dropStale(multi: DenotUnion): PreDenotation = + val compiledNow = multi.filterWithPredicate(d => d.symbol.isDefinedInCurrentRun || d.symbol.associatedFile == null // if a symbol does not have an associated file, assume it is defined // in the current run anyway. This is true for packages, and also can happen for pickling and @@ -2156,8 +2156,21 @@ object SymDenotations { ) if compiledNow.exists then compiledNow else - val youngest = d.aggregate(_.symbol.associatedFile.lastModified, _ max _) - d.filterWithPredicate(_.symbol.associatedFile.lastModified == youngest) + val assocFiles = multi.aggregate(d => Set(d.symbol.associatedFile), _ union _) + if assocFiles.size == 1 then + multi // they are all overloaded variants from the same file + else + // pick the variant(s) from the youngest class file + val lastModDate = assocFiles.map(_.lastModified).max + val youngest = assocFiles.filter(_.lastModified == lastModDate) + if youngest.size > 1 then + throw TypeError(em"""Toplevel definition $name is defined in + | ${youngest.head} + |and also in + | ${youngest.tail.head} + |One of these files should be removed from the classpath.""") + multi.filterWithPredicate(_.symbol.associatedFile == youngest.head) + end dropStale if (symbol `eq` defn.ScalaPackageClass) { val denots = super.computeNPMembersNamed(name) diff --git a/tests/run/toplevel-stale/A_1.scala b/tests/run/toplevel-stale/A_1.scala index 4026bf0b4bb0..ef762c68dcad 100644 --- a/tests/run/toplevel-stale/A_1.scala +++ b/tests/run/toplevel-stale/A_1.scala @@ -1 +1 @@ -def foo(x: Int): String = "old" \ No newline at end of file +def foo234(x: Int): String = "old" \ No newline at end of file diff --git a/tests/run/toplevel-stale/B_2.scala b/tests/run/toplevel-stale/B_2.scala index d1ac08dd50d5..af42be1e4a7d 100644 --- a/tests/run/toplevel-stale/B_2.scala +++ b/tests/run/toplevel-stale/B_2.scala @@ -1 +1,38 @@ -def foo(x: Long): String = "new" \ No newline at end of file +def foo234(x: Long): String = "new" + +// Giving the compiler something to do so that we won't get +// the same modification date as A_1.scala +import math.Ordering + +val y1 = { + val x1 = summon[Ordering[Int]] + val x2 = summon[Ordering[(Int, Int)]] + val x3 = summon[Ordering[(Int, Int, Int)]] + val x4 = summon[Ordering[(Int, Int, Int, Int)]] + val x5 = summon[Ordering[(Int, Int, Int, Int, Int)]] + val x6 = summon[Ordering[(Int, Int, Int, Int, Int, Int)]] + val x7 = summon[Ordering[(Int, Int, Int, Int, Int, Int, Int)]] + val x8 = summon[Ordering[(Int, Int, Int, Int, Int, Int, Int, Int)]] +} + +val y2 = { + val x1 = summon[Ordering[Int]] + val x2 = summon[Ordering[(Int, Int)]] + val x3 = summon[Ordering[(Int, Int, Int)]] + val x4 = summon[Ordering[(Int, Int, Int, Int)]] + val x5 = summon[Ordering[(Int, Int, Int, Int, Int)]] + val x6 = summon[Ordering[(Int, Int, Int, Int, Int, Int)]] + val x7 = summon[Ordering[(Int, Int, Int, Int, Int, Int, Int)]] + val x8 = summon[Ordering[(Int, Int, Int, Int, Int, Int, Int, Int)]] +} + +val y3 = { + val x1 = summon[Ordering[Int]] + val x2 = summon[Ordering[(Int, Int)]] + val x3 = summon[Ordering[(Int, Int, Int)]] + val x4 = summon[Ordering[(Int, Int, Int, Int)]] + val x5 = summon[Ordering[(Int, Int, Int, Int, Int)]] + val x6 = summon[Ordering[(Int, Int, Int, Int, Int, Int)]] + val x7 = summon[Ordering[(Int, Int, Int, Int, Int, Int, Int)]] + val x8 = summon[Ordering[(Int, Int, Int, Int, Int, Int, Int, Int)]] +} \ No newline at end of file diff --git a/tests/run/toplevel-stale/Test_3.scala b/tests/run/toplevel-stale/Test_3.scala index 7dbaf3857e91..3f6118d2946c 100644 --- a/tests/run/toplevel-stale/Test_3.scala +++ b/tests/run/toplevel-stale/Test_3.scala @@ -1,2 +1,2 @@ @main def Test() = - assert(foo(1) == "new") + assert(foo234(1) == "new") From 7e86070b80d67663e4c05fa7e2056a2f09accbd5 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 8 Dec 2019 14:00:20 +0100 Subject: [PATCH 15/16] Warn if files containing the same toplevel def come from different directories. --- .../tools/dotc/core/SymDenotations.scala | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 943e92d77328..aad2ab4a0a01 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -16,6 +16,7 @@ import annotation.tailrec import util.SimpleIdentityMap import util.Stats import java.util.WeakHashMap +import scala.util.control.NonFatal import config.Config import reporting.diagnostic.Message import reporting.diagnostic.messages.BadSymbolicReference @@ -2081,6 +2082,7 @@ object SymDenotations { private var packageObjsCache: List[ClassDenotation] = _ private var packageObjsRunId: RunId = NoRunId + private var ambiguityWarningIssued: Boolean = false /** The package objects in this class */ def packageObjs(implicit ctx: Context): List[ClassDenotation] = { @@ -2163,13 +2165,28 @@ object SymDenotations { // pick the variant(s) from the youngest class file val lastModDate = assocFiles.map(_.lastModified).max val youngest = assocFiles.filter(_.lastModified == lastModDate) + val chosen = youngest.head + def ambiguousFilesMsg(f: AbstractFile) = + em"""Toplevel definition $name is defined in + | $chosen + |and also in + | $f""" if youngest.size > 1 then - throw TypeError(em"""Toplevel definition $name is defined in - | ${youngest.head} - |and also in - | ${youngest.tail.head} - |One of these files should be removed from the classpath.""") - multi.filterWithPredicate(_.symbol.associatedFile == youngest.head) + throw TypeError(i"""${ambiguousFilesMsg(youngest.tail.head)} + |One of these files should be removed from the classpath.""") + def sameContainer(f: AbstractFile): Boolean = + try f.container == chosen.container catch case NonFatal(ex) => true + + // Warn if one of the older files comes from a different container. + // In that case picking the youngest file is not necessarily what we want, + // since the older file might have been loaded from a jar earlier in the + // classpath. + if !ambiguityWarningIssued then + for conflicting <- assocFiles.find(!sameContainer(_)) do + ctx.warning(i"""${ambiguousFilesMsg(youngest.tail.head)} + |Keeping only the definition in $chosen""") + ambiguityWarningIssued = true + multi.filterWithPredicate(_.symbol.associatedFile == chosen) end dropStale if (symbol `eq` defn.ScalaPackageClass) { From d1506645b9db05e3e0cbf5da15a44ac0d45ff901 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 8 Dec 2019 16:26:13 +0100 Subject: [PATCH 16/16] Warn if files containing the same toplevel def come from different directories. --- compiler/src/dotty/tools/dotc/core/SymDenotations.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index aad2ab4a0a01..87d448e672f4 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -2174,18 +2174,18 @@ object SymDenotations { if youngest.size > 1 then throw TypeError(i"""${ambiguousFilesMsg(youngest.tail.head)} |One of these files should be removed from the classpath.""") - def sameContainer(f: AbstractFile): Boolean = - try f.container == chosen.container catch case NonFatal(ex) => true // Warn if one of the older files comes from a different container. // In that case picking the youngest file is not necessarily what we want, // since the older file might have been loaded from a jar earlier in the // classpath. + def sameContainer(f: AbstractFile): Boolean = + try f.container == chosen.container catch case NonFatal(ex) => true if !ambiguityWarningIssued then for conflicting <- assocFiles.find(!sameContainer(_)) do - ctx.warning(i"""${ambiguousFilesMsg(youngest.tail.head)} + ctx.warning(i"""${ambiguousFilesMsg(conflicting)} |Keeping only the definition in $chosen""") - ambiguityWarningIssued = true + ambiguityWarningIssued = true multi.filterWithPredicate(_.symbol.associatedFile == chosen) end dropStale