Skip to content

Commit 159b3a8

Browse files
committed
Revert to the Scala 2 name encoding scheme
There were several issues with the scheme we used so far: - It's different from Scala 2, meaning that some Scala 2 methods could not be called from Dotty and vice-versa (see the added sbt-dotty/sbt-test/scala2-compat/akka/i3100.scala test for an example) - It can lead to invalid filenames on Windows (#7492) - The handling of backticks is broken: adding or removing backticks around a name changes how it's encoded. To maintain Scala 2 compat we don't have a lot of choices here, we must use the same scheme, so this commit aligns NameTransformer.scala with https://github.com/scala/scala/blob/2.13.x/src/library/scala/reflect/NameTransformer.scala Some examples: Method name | Old encoding | New encoding ----------------------------------------- a_+ | a_$plus | a_$plus `a_+` | a_+ | a_$plus `+_a` | +_a | $plus_a a_/ | a_$div | a_$div `a_/` | a_$u002F | a_$div If a Dotty method is called `def a_$plus` we won't misinterpret it as `a_+` because the method name comes from the tasty tree which stores unencoded names. On the other hand, names coming from Java / Scala 2 as well as top-level classnames might be misinterpreted as encoded names if they contain a user-written $, this is left unspecified. Fixes #3100. Fixes #5936. Fixes #7492.
1 parent a356535 commit 159b3a8

File tree

15 files changed

+130
-115
lines changed

15 files changed

+130
-115
lines changed

compiler/src/dotty/tools/backend/sjs/JSEncoding.scala

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,7 @@ object JSEncoding {
229229
fullyMangledString(sym.name)
230230
}
231231

232-
/** Work around https://github.com/lampepfl/dotty/issues/5936 by bridging
233-
* most (all?) of the gap in encoding so that Dotty.js artifacts are
234-
* compatible with the restrictions on valid IR identifier names.
235-
*/
232+
/** Convert Dotty mangled names into valid IR identifier names. */
236233
private def fullyMangledString(name: Name): String = {
237234
val base = name.mangledString
238235
val len = base.length
@@ -245,10 +242,8 @@ object JSEncoding {
245242
val c = base.charAt(i)
246243
if (c == '_')
247244
result.append("$und")
248-
else if (Character.isJavaIdentifierPart(c) || c == '.')
249-
result.append(c)
250245
else
251-
result.append("$u%04x".format(c.toInt))
246+
result.append(c)
252247
i += 1
253248
}
254249
result.toString()
@@ -257,7 +252,7 @@ object JSEncoding {
257252
var i = 0
258253
while (i != len) {
259254
val c = base.charAt(i)
260-
if (c == '_' || !Character.isJavaIdentifierPart(c))
255+
if (c == '_')
261256
return encodeFurther()
262257
i += 1
263258
}

compiler/src/dotty/tools/dotc/ast/Desugar.scala

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import Decorators._, transform.SymUtils._
99
import NameKinds.{UniqueName, EvidenceParamName, DefaultGetterName}
1010
import typer.{FrontEnd, Namer}
1111
import util.{Property, SourceFile, SourcePosition}
12-
import util.NameTransformer.avoidIllegalChars
1312
import collection.mutable.ListBuffer
1413
import reporting.diagnostic.messages._
1514
import reporting.trace
@@ -935,7 +934,7 @@ object desugar {
935934

936935
/** Invent a name for an anonympus given of type or template `impl`. */
937936
def inventGivenName(impl: Tree)(implicit ctx: Context): SimpleName =
938-
avoidIllegalChars(s"given_${inventName(impl)}".toTermName.asSimpleName)
937+
s"given_${inventName(impl)}".toTermName.asSimpleName
939938

940939
/** The normalized name of `mdef`. This means
941940
* 1. Check that the name does not redefine a Scala core class.
@@ -1250,7 +1249,7 @@ object desugar {
12501249
else {
12511250
var fileName = ctx.source.file.name
12521251
val sourceName = fileName.take(fileName.lastIndexOf('.'))
1253-
val groupName = avoidIllegalChars((sourceName ++ str.TOPLEVEL_SUFFIX).toTermName.asSimpleName)
1252+
val groupName = (sourceName ++ str.TOPLEVEL_SUFFIX).toTermName.asSimpleName
12541253
val grouped = ModuleDef(groupName, Template(emptyConstructor, Nil, Nil, EmptyValDef, nestedStats))
12551254
cpy.PackageDef(pdef)(pdef.pid, topStats :+ grouped)
12561255
}

compiler/src/dotty/tools/dotc/core/Names.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,8 @@ object Names {
147147
/** Is this name empty? */
148148
def isEmpty: Boolean
149149

150-
/** Does (the first part of) this name start with `str`? */
151-
def startsWith(str: String): Boolean = firstPart.startsWith(str)
150+
/** Does (the first part of) this name starting at index `start` starts with `str`? */
151+
def startsWith(str: String, start: Int = 0): Boolean = firstPart.startsWith(str, start)
152152

153153
/** Does (the last part of) this name end with `str`? */
154154
def endsWith(str: String): Boolean = lastPart.endsWith(str)
@@ -362,9 +362,9 @@ object Names {
362362

363363
override def isEmpty: Boolean = length == 0
364364

365-
override def startsWith(str: String): Boolean = {
365+
override def startsWith(str: String, start: Int): Boolean = {
366366
var i = 0
367-
while (i < str.length && i < length && apply(i) == str(i)) i += 1
367+
while (i < str.length && start + i < length && apply(start + i) == str(i)) i += 1
368368
i == str.length
369369
}
370370

compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,7 @@ class ClassfileParser(
686686
for (entry <- innerClasses.values) {
687687
// create a new class member for immediate inner classes
688688
if (entry.outerName == currentClassName) {
689-
val file = ctx.platform.classPath.findClassFile(entry.externalName.mangledString) getOrElse {
689+
val file = ctx.platform.classPath.findClassFile(entry.externalName.toString) getOrElse {
690690
throw new AssertionError(entry.externalName)
691691
}
692692
enterClassAndModule(entry, file, entry.jflags)

compiler/src/dotty/tools/dotc/parsing/Scanners.scala

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import core.StdNames._, core.Comments._
77
import util.SourceFile
88
import java.lang.Character.isDigit
99
import scala.internal.Chars._
10-
import util.NameTransformer.avoidIllegalChars
1110
import util.Spans.Span
1211
import config.Config
1312
import config.Printers.lexical
@@ -929,7 +928,6 @@ object Scanners {
929928
if (ch == '`') {
930929
nextChar()
931930
finishNamed(BACKQUOTED_IDENT)
932-
name = avoidIllegalChars(name)
933931
if (name.length == 0)
934932
error("empty quoted identifier")
935933
else if (name == nme.WILDCARD)

compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
7777
}
7878

7979
override def nameString(name: Name): String =
80-
if (ctx.settings.YdebugNames.value) name.debugString else NameTransformer.decodeIllegalChars(name.toString)
80+
if (ctx.settings.YdebugNames.value) name.debugString else name.toString
8181

8282
override protected def simpleNameString(sym: Symbol): String =
8383
nameString(if (ctx.property(XprintMode).isEmpty) sym.initial.name else sym.name)

compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,16 +103,10 @@ object GenericSignatures {
103103
jsig(finalType)
104104
}
105105

106-
// This will reject any name that has characters that cannot appear in
107-
// names on the JVM. Interop with Java is not guaranteed for those, so we
108-
// dont need to generate signatures for them.
109-
def sanitizeName(name: Name): String = {
110-
val nameString = name.mangledString
111-
if (nameString.forall(c => c == '.' || Character.isJavaIdentifierPart(c)))
112-
nameString
113-
else
114-
throw new UnknownSig
115-
}
106+
// This works as long as mangled names are always valid valid Java identifiers,
107+
// if we change our name encoding, we'll have to `throw new UnknownSig` here for
108+
// names which are not valid Java identifiers (see git history of this method).
109+
def sanitizeName(name: Name): String = name.mangledString
116110

117111
// Anything which could conceivably be a module (i.e. isn't known to be
118112
// a type parameter or similar) must go through here or the signature is

compiler/src/dotty/tools/dotc/util/NameTransformer.scala

Lines changed: 92 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@ import scala.annotation.internal.sharable
1313
object NameTransformer {
1414

1515
private val nops = 128
16+
private val ncodes = 26 * 26
1617

17-
@sharable private val op2code = new Array[String](nops)
18-
@sharable private val str2op = new mutable.HashMap[String, Char]
18+
private class OpCodes(val op: Char, val code: String, val next: OpCodes)
1919

20+
@sharable private val op2code = new Array[String](nops)
21+
@sharable private val code2op = new Array[OpCodes](ncodes)
2022
private def enterOp(op: Char, code: String) = {
21-
op2code(op) = code
22-
str2op(code) = op
23+
op2code(op.toInt) = code
24+
val c = (code.charAt(1) - 'a') * 26 + code.charAt(2) - 'a'
25+
code2op(c.toInt) = new OpCodes(op, code, code2op(c))
2326
}
2427

2528
/* Note: decoding assumes opcodes are only ever lowercase. */
@@ -42,99 +45,102 @@ object NameTransformer {
4245
enterOp('?', "$qmark")
4346
enterOp('@', "$at")
4447

45-
/** Expand characters that are illegal as JVM method names by `$u`, followed
46-
* by the character's unicode expansion.
47-
*/
48-
def avoidIllegalChars(name: SimpleName): SimpleName = {
49-
var i = name.length - 1
50-
while (i >= 0 && Chars.isValidJVMMethodChar(name(i))) i -= 1
51-
if (i >= 0)
52-
termName(
53-
name.toString.flatMap(ch =>
54-
if (Chars.isValidJVMMethodChar(ch)) ch.toString else "$u%04X".format(ch.toInt)))
55-
else name
56-
}
57-
58-
/** Decode expanded characters starting with `$u`, followed by the character's unicode expansion. */
59-
def decodeIllegalChars(name: String): String =
60-
if (name.contains("$u")) {
61-
val sb = new mutable.StringBuilder()
62-
var i = 0
63-
while (i < name.length)
64-
if (i < name.length - 5 && name(i) == '$' && name(i + 1) == 'u') {
65-
val numbers = name.substring(i + 2, i + 6)
66-
try sb.append(Integer.valueOf(name.substring(i + 2, i + 6), 16).toChar)
67-
catch {
68-
case _: java.lang.NumberFormatException =>
69-
sb.append("$u").append(numbers)
70-
}
71-
i += 6
72-
}
73-
else {
74-
sb.append(name(i))
75-
i += 1
76-
}
77-
sb.result()
78-
}
79-
else name
80-
81-
/** Replace operator symbols by corresponding expansion strings.
82-
*
83-
* @param name the string to encode
84-
* @return the string with all recognized opchars replaced with their encoding
85-
*
86-
* Operator symbols are only recognized if they make up the whole name, or
87-
* if they make up the last part of the name which follows a `_`.
48+
/** Replace operator symbols by corresponding expansion strings, and replace
49+
* characters that are not valid Java identifiers by "$u" followed by the
50+
* character's unicode expansion.
8851
*/
8952
def encode(name: SimpleName): SimpleName = {
90-
def loop(len: Int, ops: List[String]): SimpleName = {
91-
def convert =
92-
if (ops.isEmpty) name
93-
else {
94-
val buf = new java.lang.StringBuilder
95-
buf.append(chrs, name.start, len)
96-
for (op <- ops) buf.append(op)
97-
termName(buf.toString)
53+
var buf: StringBuilder = null
54+
val len = name.length
55+
var i = 0
56+
while (i < len) {
57+
val c = name(i)
58+
if (c < nops && (op2code(c.toInt) ne null)) {
59+
if (buf eq null) {
60+
buf = new StringBuilder()
61+
buf.append(name.sliceToString(0, i))
62+
}
63+
buf.append(op2code(c.toInt))
64+
/* Handle glyphs that are not valid Java/JVM identifiers */
65+
}
66+
else if (!Character.isJavaIdentifierPart(c)) {
67+
if (buf eq null) {
68+
buf = new StringBuilder()
69+
buf.append(name.sliceToString(0, i))
9870
}
99-
if (len == 0 || name(len - 1) == '_') convert
100-
else {
101-
val ch = name(len - 1)
102-
if (ch <= nops && op2code(ch) != null)
103-
loop(len - 1, op2code(ch) :: ops)
104-
else if (Chars.isSpecial(ch))
105-
loop(len - 1, ch.toString :: ops)
106-
else name
71+
buf.append("$u%04X".format(c.toInt))
10772
}
73+
else if (buf ne null) {
74+
buf.append(c)
75+
}
76+
i += 1
10877
}
109-
loop(name.length, Nil)
78+
if (buf eq null) name else termName(buf.toString)
11079
}
11180

112-
/** Replace operator expansions by the operators themselves.
113-
* Operator expansions are only recognized if they make up the whole name, or
114-
* if they make up the last part of the name which follows a `_`.
81+
/** Replace operator expansions by the operators themselves,
82+
* and decode `$u....` expansions into unicode characters.
11583
*/
11684
def decode(name: SimpleName): SimpleName = {
117-
def loop(len: Int, ops: List[Char]): SimpleName = {
118-
def convert =
119-
if (ops.isEmpty) name
120-
else {
121-
val buf = new java.lang.StringBuilder
122-
buf.append(chrs, name.start, len)
123-
for (op <- ops) buf.append(op)
124-
termName(buf.toString)
125-
}
126-
if (len == 0 || name(len - 1) == '_') convert
127-
else if (Chars.isSpecial(name(len - 1))) loop(len - 1, name(len - 1) :: ops)
128-
else {
129-
val idx = name.lastIndexOf('$', len - 1)
130-
if (idx >= 0 && idx + 2 < len)
131-
str2op.get(name.sliceToString(idx, len)) match {
132-
case Some(ch) => loop(idx, ch :: ops)
133-
case None => name
85+
//System.out.println("decode: " + name);//DEBUG
86+
var buf: StringBuilder = null
87+
val len = name.length
88+
var i = 0
89+
while (i < len) {
90+
var ops: OpCodes = null
91+
var unicode = false
92+
val c = name(i)
93+
if (c == '$' && i + 2 < len) {
94+
val ch1 = name(i + 1)
95+
if ('a' <= ch1 && ch1 <= 'z') {
96+
val ch2 = name(i + 2)
97+
if ('a' <= ch2 && ch2 <= 'z') {
98+
ops = code2op((ch1 - 'a') * 26 + ch2 - 'a')
99+
while ((ops ne null) && !name.startsWith(ops.code, i)) ops = ops.next
100+
if (ops ne null) {
101+
if (buf eq null) {
102+
buf = new StringBuilder()
103+
buf.append(name.sliceToString(0, i))
104+
}
105+
buf.append(ops.op)
106+
i += ops.code.length()
107+
}
108+
/* Handle the decoding of Unicode glyphs that are
109+
* not valid Java/JVM identifiers */
110+
} else if ((len - i) >= 6 && // Check that there are enough characters left
111+
ch1 == 'u' &&
112+
((Character.isDigit(ch2)) ||
113+
('A' <= ch2 && ch2 <= 'F'))) {
114+
/* Skip past "$u", next four should be hexadecimal */
115+
val hex = name.sliceToString(i+2, i+6)
116+
try {
117+
val str = Integer.parseInt(hex, 16).toChar
118+
if (buf eq null) {
119+
buf = new StringBuilder()
120+
buf.append(name.sliceToString(0, i))
121+
}
122+
buf.append(str)
123+
/* 2 for "$u", 4 for hexadecimal number */
124+
i += 6
125+
unicode = true
126+
} catch {
127+
case _:NumberFormatException =>
128+
/* `hex` did not decode to a hexadecimal number, so
129+
* do nothing. */
130+
}
134131
}
135-
else name
132+
}
133+
}
134+
/* If we didn't see an opcode or encoded Unicode glyph, and the
135+
buffer is non-empty, write the current character and advance
136+
one */
137+
if ((ops eq null) && !unicode) {
138+
if (buf ne null)
139+
buf.append(c)
140+
i += 1
136141
}
137142
}
138-
loop(name.length, Nil)
143+
//System.out.println("= " + (if (buf == null) name else buf.toString()));//DEBUG
144+
if (buf eq null) name else termName(buf.toString)
139145
}
140146
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
scalaVersion := sys.props("plugin.scalaVersion")
2+
3+
libraryDependencies ++= {
4+
Seq(
5+
("com.typesafe.akka" %% "akka-http" % "10.1.10"),
6+
("com.typesafe.akka" %% "akka-stream" % "2.6.0")
7+
).map(_.withDottyCompat(scalaVersion.value))
8+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import akka.actor.ActorSystem
2+
import akka.http.scaladsl.Http
3+
import akka.http.scaladsl.model._
4+
import akka.http.scaladsl.server.Directives._
5+
import akka.stream.ActorMaterializer
6+
import scala.io.StdIn
7+
8+
object WebServer {
9+
def main(args: Array[String]): Unit = {
10+
val x = ContentTypes.`text/html(UTF-8)`
11+
}
12+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % sys.props("plugin.version"))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
> compile

tests/generic-java-signatures/invalidNames.check

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
$bang$u005B$u005D$colon$u003B$bang$bang <: java.util.Date

0 commit comments

Comments
 (0)