diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 17ddc93a10f2..6f89f3c84911 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -5,7 +5,6 @@ package core import Symbols.*, Types.*, Contexts.*, Flags.*, Names.*, StdNames.*, Phases.* import Flags.JavaDefined import Uniques.unique -import TypeOps.makePackageObjPrefixExplicit import backend.sjs.JSDefinitions import transform.ExplicitOuter.* import transform.ValueClasses.* diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index 9c9bf332d1f4..d801362fd475 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -573,36 +573,6 @@ object TypeOps: widenMap(tp) } - /** If `tpe` is of the form `p.x` where `p` refers to a package - * but `x` is not owned by a package, expand it to - * - * p.package.x - */ - def makePackageObjPrefixExplicit(tpe: NamedType)(using Context): Type = { - def tryInsert(pkgClass: SymDenotation): Type = pkgClass match { - case pkg: PackageClassDenotation => - var sym = tpe.symbol - if !sym.exists && tpe.denot.isOverloaded then - // we know that all alternatives must come from the same package object, since - // otherwise we would get "is already defined" errors. So we can take the first - // symbol we see. - sym = tpe.denot.alternatives.head.symbol - val pobj = pkg.packageObjFor(sym) - if (pobj.exists) tpe.derivedSelect(pobj.termRef) - else tpe - case _ => - tpe - } - if (tpe.symbol.isRoot) - tpe - else - tpe.prefix match { - case pre: ThisType if pre.cls.is(Package) => tryInsert(pre.cls) - case pre: TermRef if pre.symbol.is(Package) => tryInsert(pre.symbol.moduleClass) - case _ => tpe - } - } - /** An argument bounds violation is a triple consisting of * - the argument tree * - a string "upper" or "lower" indicating which bound is violated diff --git a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala index 8167bf532e1a..fa4e3e31ec32 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala @@ -3,7 +3,10 @@ package dotc package core import TypeErasure.ErasedValueType -import Types.*, Contexts.*, Symbols.*, Flags.*, Decorators.* +import Types.*, Contexts.*, Symbols.*, Flags.*, Decorators.*, SymDenotations.* +import Names.{Name, TermName} +import Constants.Constant + import Names.Name import StdNames.nme @@ -127,6 +130,37 @@ class TypeUtils: case mt: MethodType => mt.isImplicitMethod || mt.resType.takesImplicitParams case _ => false + + /** If `self` is of the form `p.x` where `p` refers to a package + * but `x` is not owned by a package, expand it to + * + * p.package.x + */ + def makePackageObjPrefixExplicit(using Context): Type = + def tryInsert(tpe: NamedType, pkgClass: SymDenotation): Type = pkgClass match + case pkg: PackageClassDenotation => + var sym = tpe.symbol + if !sym.exists && tpe.denot.isOverloaded then + // we know that all alternatives must come from the same package object, since + // otherwise we would get "is already defined" errors. So we can take the first + // symbol we see. + sym = tpe.denot.alternatives.head.symbol + val pobj = pkg.packageObjFor(sym) + if pobj.exists then tpe.derivedSelect(pobj.termRef) + else tpe + case _ => + tpe + self match + case tpe: NamedType => + if tpe.symbol.isRoot then + tpe + else + tpe.prefix match + case pre: ThisType if pre.cls.is(Package) => tryInsert(tpe, pre.cls) + case pre: TermRef if pre.symbol.is(Package) => tryInsert(tpe, pre.symbol.moduleClass) + case _ => tpe + case tpe => tpe + /** The constructors of this type that are applicable to `argTypes`, without needing * an implicit conversion. Curried constructors are always excluded. * @param adaptVarargs if true, allow a constructor with just a varargs argument to diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index e76b333428f5..23fcc4da861b 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -1189,7 +1189,7 @@ class TreeUnpickler(reader: TastyReader, val tpe0 = name match case name: TypeName => TypeRef(qualType, name, denot) case name: TermName => TermRef(qualType, name, denot) - val tpe = TypeOps.makePackageObjPrefixExplicit(tpe0) + val tpe = tpe0.makePackageObjPrefixExplicit ConstFold.Select(untpd.Select(qual, name).withType(tpe)) def completeSelect(name: Name, sig: Signature, target: Name): Select = diff --git a/compiler/src/dotty/tools/dotc/profile/JsonNameTransformer.scala b/compiler/src/dotty/tools/dotc/profile/JsonNameTransformer.scala new file mode 100644 index 000000000000..8777a95c33cf --- /dev/null +++ b/compiler/src/dotty/tools/dotc/profile/JsonNameTransformer.scala @@ -0,0 +1,46 @@ +package dotty.tools.dotc.profile + +import scala.annotation.internal.sharable + +// Based on NameTransformer but dedicated for JSON encoding rules +object JsonNameTransformer { + private val nops = 128 + + @sharable private val op2code = new Array[String](nops) + private def enterOp(op: Char, code: String) = op2code(op.toInt) = code + + enterOp('\"', "\\\"") + enterOp('\\', "\\\\") + // enterOp('/', "\\/") // optional, no need for escaping outside of html context + enterOp('\b', "\\b") + enterOp('\f', "\\f") + enterOp('\n', "\\n") + enterOp('\r', "\\r") + enterOp('\t', "\\t") + + def encode(name: String): String = { + var buf: StringBuilder = null.asInstanceOf + val len = name.length + var i = 0 + while (i < len) { + val c = name(i) + if (c < nops && (op2code(c.toInt) ne null)) { + if (buf eq null) { + buf = new StringBuilder() + buf.append(name.subSequence(0, i)) + } + buf.append(op2code(c.toInt)) + } else if (c <= 0x1F || c >= 0x7F) { + if (buf eq null) { + buf = new StringBuilder() + buf.append(name.subSequence(0, i)) + } + buf.append("\\u%04X".format(c.toInt)) + } else if (buf ne null) { + buf.append(c) + } + i += 1 + } + if (buf eq null) name else buf.toString + } +} \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/profile/Profiler.scala b/compiler/src/dotty/tools/dotc/profile/Profiler.scala index 69a806215ddd..ab3e73468385 100644 --- a/compiler/src/dotty/tools/dotc/profile/Profiler.scala +++ b/compiler/src/dotty/tools/dotc/profile/Profiler.scala @@ -273,7 +273,7 @@ private [profile] class RealProfiler(reporter : ProfileReporter)(using Context) override def beforePhase(phase: Phase): (TracedEventId, ProfileSnap) = { assert(mainThread eq Thread.currentThread()) traceThreadSnapshotCounters() - val eventId = traceDurationStart(Category.Phase, phase.phaseName) + val eventId = traceDurationStart(Category.Phase, escapeSpecialChars(phase.phaseName)) if (ctx.settings.YprofileRunGcBetweenPhases.value.contains(phase.toString)) doGC() if (ctx.settings.YprofileExternalTool.value.contains(phase.toString)) { @@ -287,7 +287,7 @@ private [profile] class RealProfiler(reporter : ProfileReporter)(using Context) assert(mainThread eq Thread.currentThread()) if chromeTrace != null then traceThreadSnapshotCounters() - traceDurationStart(Category.File, unit.source.name) + traceDurationStart(Category.File, escapeSpecialChars(unit.source.name)) else TracedEventId.Empty } @@ -325,7 +325,7 @@ private [profile] class RealProfiler(reporter : ProfileReporter)(using Context) then EmptyCompletionEvent else val completionName = this.completionName(root, associatedFile) - val event = TracedEventId(associatedFile.name) + val event = TracedEventId(escapeSpecialChars(associatedFile.name)) chromeTrace.traceDurationEventStart(Category.Completion.name, "↯", colour = "thread_state_sleeping") chromeTrace.traceDurationEventStart(Category.File.name, event) chromeTrace.traceDurationEventStart(Category.Completion.name, completionName) @@ -350,8 +350,13 @@ private [profile] class RealProfiler(reporter : ProfileReporter)(using Context) if chromeTrace != null then chromeTrace.traceDurationEventEnd(category.name, event, colour) - private def symbolName(sym: Symbol): String = s"${sym.showKind} ${sym.showName}" - private def completionName(root: Symbol, associatedFile: AbstractFile): String = + private inline def escapeSpecialChars(value: String): String = + JsonNameTransformer.encode(value) + + private def symbolName(sym: Symbol): String = escapeSpecialChars: + s"${sym.showKind} ${sym.showName}" + + private def completionName(root: Symbol, associatedFile: AbstractFile): String = escapeSpecialChars: def isTopLevel = root.owner != NoSymbol && root.owner.is(Flags.Package) if root.is(Flags.Package) || isTopLevel then root.javaBinaryName diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index c764b035cba1..5b5a59335184 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -81,7 +81,7 @@ trait TypeAssigner { defn.FromJavaObjectType else tpe match case tpe: NamedType => - val tpe1 = TypeOps.makePackageObjPrefixExplicit(tpe) + val tpe1 = tpe.makePackageObjPrefixExplicit if tpe1 ne tpe then accessibleType(tpe1, superAccess) else diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 159e93dfc9fb..5cf447eb2a48 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -306,7 +306,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // so we ignore that import. if reallyExists(denot) && !isScalaJsPseudoUnion then if unimported.isEmpty || !unimported.contains(pre.termSymbol) then - return pre.select(name, denot) + return pre.select(name, denot).makePackageObjPrefixExplicit case _ => if imp.importSym.isCompleting then report.warning(i"cyclic ${imp.importSym}, ignored", pos) @@ -466,7 +466,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer defDenot.symbol.owner else curOwner - effectiveOwner.thisType.select(name, defDenot) + effectiveOwner.thisType.select(name, defDenot).makePackageObjPrefixExplicit } if !curOwner.is(Package) || isDefinedInCurrentUnit(defDenot) then result = checkNewOrShadowed(found, Definition) // no need to go further out, we found highest prec entry diff --git a/compiler/test/dotty/tools/DottyTest.scala b/compiler/test/dotty/tools/DottyTest.scala index 7ccbc09a4c92..834384d1e478 100644 --- a/compiler/test/dotty/tools/DottyTest.scala +++ b/compiler/test/dotty/tools/DottyTest.scala @@ -46,7 +46,7 @@ trait DottyTest extends ContextEscapeDetection { protected def defaultCompiler: Compiler = new Compiler() - private def compilerWithChecker(phase: String)(assertion: (tpd.Tree, Context) => Unit) = new Compiler { + protected def compilerWithChecker(phase: String)(assertion: (tpd.Tree, Context) => Unit) = new Compiler { private val baseCompiler = defaultCompiler diff --git a/compiler/test/dotty/tools/dotc/profile/TraceNameManglingTest.scala b/compiler/test/dotty/tools/dotc/profile/TraceNameManglingTest.scala new file mode 100644 index 000000000000..977b67740f88 --- /dev/null +++ b/compiler/test/dotty/tools/dotc/profile/TraceNameManglingTest.scala @@ -0,0 +1,133 @@ +package dotty.tools.dotc.profile + +import org.junit.Assert.* +import org.junit.* + +import scala.annotation.tailrec +import dotty.tools.DottyTest +import dotty.tools.dotc.util.SourceFile +import dotty.tools.dotc.core.Contexts.FreshContext +import java.nio.file.Files +import java.util.Locale + +class TraceNameManglingTest extends DottyTest { + + override protected def initializeCtx(fc: FreshContext): Unit = { + super.initializeCtx(fc) + val tmpDir = Files.createTempDirectory("trace_name_mangling_test").nn + fc.setSetting(fc.settings.YprofileEnabled, true) + fc.setSetting( + fc.settings.YprofileTrace, + tmpDir.resolve("trace.json").nn.toAbsolutePath().toString() + ) + fc.setSetting( + fc.settings.YprofileDestination, + tmpDir.resolve("profiler.out").nn.toAbsolutePath().toString() + ) + } + + @Test def escapeBackslashes(): Unit = { + val isWindows = sys.props("os.name").toLowerCase(Locale.ROOT) == "windows" + val filename = if isWindows then "/.scala" else "\\.scala" + checkTraceEvents( + """ + |class /\ : + | var /\ = ??? + |object /\{ + | def /\ = ??? + |}""".stripMargin, + filename = filename + )( + Set( + raw"class /\\", + raw"object /\\", + raw"method /\\", + raw"variable /\\", + raw"setter /\\_=" + ).map(TraceEvent("typecheck", _)) + ++ Set( + TraceEvent("file", if isWindows then "/.scala" else "\\\\.scala") + ) + ) + } + + @Test def escapeDoubleQuotes(): Unit = { + val filename = "\"quoted\".scala" + checkTraceEvents( + """ + |class `"QuotedClass"`: + | var `"quotedVar"` = ??? + |object `"QuotedObject"` { + | def `"quotedMethod"` = ??? + |}""".stripMargin, + filename = filename + ): + Set( + raw"class \"QuotedClass\"", + raw"object \"QuotedObject\"", + raw"method \"quotedMethod\"", + raw"variable \"quotedVar\"" + ).map(TraceEvent("typecheck", _)) + ++ Set(TraceEvent("file", "\\\"quoted\\\".scala")) + } + @Test def escapeNonAscii(): Unit = { + val filename = "unic😀de.scala" + checkTraceEvents( + """ + |class ΩUnicodeClass: + | var `中文Var` = ??? + |object ΩUnicodeObject { + | def 中文Method = ??? + |}""".stripMargin, + filename = filename + ): + Set( + "class \\u03A9UnicodeClass", + "object \\u03A9UnicodeObject", + "method \\u4E2D\\u6587Method", + "variable \\u4E2D\\u6587Var" + ).map(TraceEvent("typecheck", _)) + ++ Set(TraceEvent("file", "unic\\uD83D\\uDE00de.scala")) + } + + case class TraceEvent(category: String, name: String) + private def compileWithTracer( + code: String, + filename: String, + afterPhase: String = "typer" + )(checkEvents: Seq[TraceEvent] => Unit) = { + val runCtx = locally: + val source = SourceFile.virtual(filename, code) + val c = compilerWithChecker(afterPhase) { (_, _) => () } + val run = c.newRun + run.compileSources(List(source)) + run.runContext + assert(!runCtx.reporter.hasErrors, "compilation failed") + val outfile = ctx.settings.YprofileTrace.value + checkEvents: + scala.io.Source + .fromFile(outfile) + .getLines() + .collect: + case s"""${_}"cat":"${category}","name":${name},"ph":${_}""" => + TraceEvent(category, name.stripPrefix("\"").stripSuffix("\"")) + .distinct.toSeq + } + + private def checkTraceEvents(code: String, filename: String = "test")(expected: Set[TraceEvent]): Unit = { + compileWithTracer(code, filename = filename, afterPhase = "typer"){ events => + val missing = expected.diff(events.toSet) + def showFound = events + .groupBy(_.category) + .collect: + case (category, events) + if expected.exists(_.category == category) => + s"- $category: [${events.map(_.name).mkString(", ")}]" + .mkString("\n") + assert( + missing.isEmpty, + s"""Missing ${missing.size} names [${missing.mkString(", ")}] in events, got:\n${showFound}""" + ) + } + } +} diff --git a/tests/pos/i18097.1.scala b/tests/pos/i18097.1.scala new file mode 100644 index 000000000000..b7b57467e3b0 --- /dev/null +++ b/tests/pos/i18097.1.scala @@ -0,0 +1,22 @@ +opaque type Pos = Double + +object Pos: + extension (x: Pos) + def mult1(y: Pos): Pos = x * y + +extension (x: Pos) + def mult2(y: Pos): Pos = x * y + +class Test: + def test(key: String, a: Pos, b: Pos): Unit = + val tup1 = new Tuple1(Pos.mult1(a)(b)) + val res1: Pos = tup1._1 + + val tup2 = new Tuple1(a.mult1(b)) + val res2: Pos = tup2._1 + + val tup3 = new Tuple1(mult2(a)(b)) + val res3: Pos = tup3._1 + + val tup4 = new Tuple1(a.mult2(b)) + val res4: Pos = tup4._1 // was error: Found: (tup4._4 : Double) Required: Pos diff --git a/tests/pos/i18097.2.scala b/tests/pos/i18097.2.scala new file mode 100644 index 000000000000..c676479aab42 --- /dev/null +++ b/tests/pos/i18097.2.scala @@ -0,0 +1,13 @@ +opaque type Namespace = List[String] + +object Namespace: + def apply(head: String): Namespace = List(head) + +extension (ns: Namespace) + def appended(segment: String): Namespace = ns.appended(segment) + +object Main: + def main(args: Array[String]): Unit = + val a: Namespace = Namespace("a") + .appended("B") + .appended("c") // was error: Found: List[String] Required: Namespace diff --git a/tests/pos/i18097.2.works.scala b/tests/pos/i18097.2.works.scala new file mode 100644 index 000000000000..3ba8e056a4a5 --- /dev/null +++ b/tests/pos/i18097.2.works.scala @@ -0,0 +1,13 @@ +object Main: + opaque type Namespace = List[String] + + object Namespace: + def apply(head: String): Namespace = List(head) + + extension (ns: Namespace) + def appended(segment: String): Namespace = ns.appended(segment) + + def main(args: Array[String]): Unit = + val a: Namespace = Namespace("a") + .appended("B") + .appended("c") diff --git a/tests/pos/i18097.3/Opaque.scala b/tests/pos/i18097.3/Opaque.scala new file mode 100644 index 000000000000..cb9c9eaedfb3 --- /dev/null +++ b/tests/pos/i18097.3/Opaque.scala @@ -0,0 +1,9 @@ +package test + +type Foo = Unit +val bar: Foo = () + +opaque type Opaque = Unit + +extension (foo: Foo) + def go: Option[Opaque] = ??? diff --git a/tests/pos/i18097.3/Test.scala b/tests/pos/i18097.3/Test.scala new file mode 100644 index 000000000000..38f2199944c2 --- /dev/null +++ b/tests/pos/i18097.3/Test.scala @@ -0,0 +1,13 @@ +package test + +final case class Test(value: Opaque) + +def test: Test = + bar.go match + case Some(value) => Test(value) // was error: Found: (value : Unit) Required: test.Opaque + case _ => ??? + +def test2: Test = + go(bar) match + case Some(value) => Test(value) + case _ => ??? diff --git a/tests/pos/i18097.orig.scala b/tests/pos/i18097.orig.scala new file mode 100644 index 000000000000..092a904f6de4 --- /dev/null +++ b/tests/pos/i18097.orig.scala @@ -0,0 +1,20 @@ +opaque type PositiveNumber = Double + +object PositiveNumber: + extension (x: PositiveNumber) + def mult1(other: PositiveNumber): PositiveNumber = + x * other + +extension (x: PositiveNumber) + def mult2(other: PositiveNumber): PositiveNumber = + x * other + +object Test: + def multMap1[A](x: Map[A, PositiveNumber], num: PositiveNumber): Map[A, PositiveNumber] = x.map((key, value) => key -> value.mult1(num)).toMap + + def multMap2[A](x: Map[A, PositiveNumber], num: PositiveNumber): Map[A, PositiveNumber] = x.map((key, value) => key -> value.mult2(num)).toMap // was error +// ^ +// Cannot prove that (A, Double) <:< (A, V2). +// +// where: V2 is a type variable with constraint <: PositiveNumber + def multMap2_2[A](x: Map[A, PositiveNumber], num: PositiveNumber): Map[A, PositiveNumber] = x.map((key, value) => key -> mult2(value)(num)).toMap