Skip to content

Commit 0d36979

Browse files
authored
Merge pull request #9601 from dotty-staging/java-object-any-5
Improve handling of references to `Object` coming from Java code
2 parents 387c562 + 173899b commit 0d36979

37 files changed

+346
-98
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1625,7 +1625,7 @@ object desugar {
16251625
Apply(Select(Apply(scalaDot(nme.StringContext), strs), id).withSpan(tree.span), elems)
16261626
case PostfixOp(t, op) =>
16271627
if ((ctx.mode is Mode.Type) && !isBackquoted(op) && op.name == tpnme.raw.STAR) {
1628-
if ctx.compilationUnit.isJava then
1628+
if ctx.isJava then
16291629
AppliedTypeTree(ref(defn.RepeatedParamType), t)
16301630
else
16311631
Annotated(

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,13 @@ object Contexts {
363363
/** Does current phase use an erased types interpretation? */
364364
final def erasedTypes = phase.erasedTypes
365365

366+
/** Are we in a Java compilation unit? */
367+
final def isJava: Boolean =
368+
// FIXME: It would be much nicer if compilationUnit was non-nullable,
369+
// perhaps we need to introduce a `NoCompilationUnit` compilation unit
370+
// to be used as a default value.
371+
compilationUnit != null && compilationUnit.isJava
372+
366373
/** Is current phase after FrontEnd? */
367374
final def isAfterTyper = base.isAfterTyper(phase)
368375

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

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ class Definitions {
195195
RootClass, nme.EMPTY_PACKAGE, (emptypkg, emptycls) => ctx.base.rootLoader(emptypkg)).entered
196196
@tu lazy val EmptyPackageClass: ClassSymbol = EmptyPackageVal.moduleClass.asClass
197197

198-
/** A package in which we can place all methods that are interpreted specially by the compiler */
198+
/** A package in which we can place all methods and types that are interpreted specially by the compiler */
199199
@tu lazy val OpsPackageVal: TermSymbol = newCompletePackageSymbol(RootClass, nme.OPS_PACKAGE).entered
200200
@tu lazy val OpsPackageClass: ClassSymbol = OpsPackageVal.moduleClass.asClass
201201

@@ -310,6 +310,103 @@ class Definitions {
310310
}
311311
def ObjectType: TypeRef = ObjectClass.typeRef
312312

313+
/** A type alias of Object used to represent any reference to Object in a Java
314+
* signature, the secret sauce is that subtype checking treats it specially:
315+
*
316+
* tp <:< FromJavaObject
317+
*
318+
* is equivalent to:
319+
*
320+
* tp <:< Any
321+
*
322+
* This is useful to avoid usability problems when interacting with Java
323+
* code where Object is the top type. This is safe because this type will
324+
* only appear in signatures of Java definitions in positions where `Object`
325+
* might appear, let's enumerate all possible cases this gives us:
326+
*
327+
* 1. At the top level:
328+
*
329+
* // A.java
330+
* void meth1(Object arg) {}
331+
* <T> void meth2(T arg) {} // T implicitly extends Object
332+
*
333+
* // B.scala
334+
* meth1(1) // OK
335+
* meth2(1) // OK
336+
*
337+
* This is safe even though Int is not a subtype of Object, because Erasure
338+
* will detect the mismatch and box the value type.
339+
*
340+
* 2. In a class type parameter:
341+
*
342+
* // A.java
343+
* void meth3(scala.List<Object> arg) {}
344+
* <T> void meth4(scala.List<T> arg) {}
345+
*
346+
* // B.scala
347+
* meth3(List[Int](1)) // OK
348+
* meth4(List[Int](1)) // OK
349+
*
350+
* At erasure, type parameters are removed and value types are boxed.
351+
*
352+
* 3. As the type parameter of an array:
353+
*
354+
* // A.java
355+
* void meth5(Object[] arg) {}
356+
* <T> void meth6(T[] arg) {}
357+
*
358+
* // B.scala
359+
* meth5(Array[Int](1)) // error: Array[Int] is not a subtype of Array[Object]
360+
* meth6(Array[Int](1)) // error: Array[Int] is not a subtype of Array[T & Object]
361+
*
362+
*
363+
* This is a bit more subtle: at erasure, Arrays keep their type parameter,
364+
* and primitive Arrays are not subtypes of reference Arrays on the JVM,
365+
* so we can't pass an Array of Int where a reference Array is expected.
366+
* Array is invariant in Scala, so `meth5` is safe even if we use `FromJavaObject`,
367+
* but generic Arrays are treated specially: we always add `& Object` (and here
368+
* we mean the normal java.lang.Object type) to these types when they come from
369+
* Java signatures (see `translateJavaArrayElementType`), this ensure that `meth6`
370+
* is safe to use.
371+
*
372+
* 4. As the repeated argument of a varargs method:
373+
*
374+
* // A.java
375+
* void meth7(Object... args) {}
376+
* <T> void meth8(T... args) {}
377+
*
378+
* // B.scala
379+
* meth7(1) // OK
380+
* meth8(1) // OK
381+
* val ai = Array[Int](1)
382+
* meth7(ai: _*) // OK (will copy the array)
383+
* meth8(ai: _*) // OK (will copy the array)
384+
*
385+
* Java repeated arguments are erased to arrays, so it would be safe to treat
386+
* them in the same way: add an `& Object` to the parameter type to disallow
387+
* passing primitives, but that would be very inconvenient as it is common to
388+
* want to pass a primitive to an Object repeated argument (e.g.
389+
* `String.format("foo: %d", 1)`). So instead we type them _without_ adding the
390+
* `& Object` and let `ElimRepeated` take care of doing any necessary adaptation
391+
* (note that adapting a primitive array to a reference array requires
392+
* copying the whole array, so this transformation only preserves semantics
393+
* if the callee does not try to mutate the varargs array which is a reasonable
394+
* assumption to make).
395+
*
396+
*
397+
* This mechanism is similar to `ObjectTpeJavaRef` in Scala 2, except that we
398+
* create a new symbol with its own name, this is needed because this type
399+
* can show up in inferred types and therefore needs to be preserved when
400+
* pickling so that unpickled trees pass `-Ycheck`.
401+
*
402+
* Note that by default we pretty-print `FromJavaObject` as `Object` or simply omit it
403+
* if it's the sole upper-bound of a type parameter, use `-Yprint-debug` to explicitly
404+
* display it.
405+
*/
406+
@tu lazy val FromJavaObjectSymbol: TypeSymbol =
407+
newPermanentSymbol(OpsPackageClass, tpnme.FromJavaObject, JavaDefined, TypeAlias(ObjectType)).entered
408+
def FromJavaObjectType: TypeRef = FromJavaObjectSymbol.typeRef
409+
313410
@tu lazy val AnyRefAlias: TypeSymbol = enterAliasType(tpnme.AnyRef, ObjectType)
314411
def AnyRefType: TypeRef = AnyRefAlias.typeRef
315412

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ object StdNames {
199199
final val Null: N = "Null"
200200
final val UncheckedNull: N = "UncheckedNull"
201201
final val Object: N = "Object"
202+
final val FromJavaObject: N = "<FromJavaObject>"
202203
final val Product: N = "Product"
203204
final val PartialFunction: N = "PartialFunction"
204205
final val PrefixType: N = "PrefixType"

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -413,24 +413,29 @@ class TypeApplications(val self: Type) extends AnyVal {
413413
def translateToRepeated(from: ClassSymbol)(using Context): Type =
414414
translateParameterized(from, defn.RepeatedParamClass)
415415

416-
/** Translate `T` by `T & Object` in the situations where an `Array[T]`
416+
/** Translate `T` to `T & Object` in the situations where an `Array[T]`
417417
* coming from Java would need to be interpreted as an `Array[T & Object]`
418418
* to be erased correctly.
419419
*
420-
* This is necessary because a fully generic Java array erases to an array of Object,
421-
* whereas a fully generic Java array erases to Object to allow primitive arrays
422-
* as subtypeS.
420+
* `Object` is the top-level type in Java, but when it appears in a Java
421+
* signature we replace it by a special `FromJavaObject` type for
422+
* convenience, this in turns requires us to special-case generic arrays as
423+
* described in case 3 in the documentation of `FromJavaObjectSymbol`. This
424+
* is necessary because a fully generic Java array erases to an array of
425+
* Object, whereas a fully generic Scala array erases to Object to allow
426+
* primitive arrays as subtypes.
423427
*
424428
* Note: According to
425429
* <http://cr.openjdk.java.net/~briangoetz/valhalla/sov/02-object-model.html>,
426-
* in the future the JVM will consider that:
430+
* it's possible that future JVMs will consider that:
427431
*
428432
* int[] <: Integer[] <: Object[]
429433
*
430434
* So hopefully our grand-children will not have to deal with this non-sense!
431435
*/
432436
def translateJavaArrayElementType(using Context): Type =
433-
if self.typeSymbol.isAbstractOrParamType && !self.derivesFrom(defn.ObjectClass) then
437+
// A type parameter upper-bounded solely by `FromJavaObject` has `ObjectClass` as its classSymbol
438+
if self.typeSymbol.isAbstractOrParamType && (self.classSymbol eq defn.ObjectClass) then
434439
AndType(self, defn.ObjectType)
435440
else
436441
self

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

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,11 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
275275
case _ =>
276276
secondTry
277277
end compareNamed
278-
compareNamed(tp1, tp2)
278+
// See the documentation of `FromJavaObjectSymbol`
279+
if !ctx.erasedTypes && tp2.isFromJavaObject then
280+
recur(tp1, defn.AnyType)
281+
else
282+
compareNamed(tp1, tp2)
279283
case tp2: ProtoType =>
280284
isMatchedByProto(tp2, tp1)
281285
case tp2: BoundType =>
@@ -1769,35 +1773,15 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
17691773
}
17701774

17711775
/** Do the parameter types of `tp1` and `tp2` match in a way that allows `tp1`
1772-
* to override `tp2` ? This is the case if they're pairwise =:=, as a special
1773-
* case, we allow `Any` in Java methods to match `Object`.
1776+
* to override `tp2` ? This is the case if they're pairwise `=:=`.
17741777
*/
17751778
def matchingMethodParams(tp1: MethodType, tp2: MethodType): Boolean = {
17761779
def loop(formals1: List[Type], formals2: List[Type]): Boolean = formals1 match {
17771780
case formal1 :: rest1 =>
17781781
formals2 match {
17791782
case formal2 :: rest2 =>
17801783
val formal2a = if (tp2.isParamDependent) formal2.subst(tp2, tp1) else formal2
1781-
// The next two definitions handle the special case mentioned above, where
1782-
// the Java argument has type 'Any', and the Scala argument has type 'Object' or
1783-
// 'Object|Null', depending on whether explicit nulls are enabled.
1784-
def formal1IsObject =
1785-
if (ctx.explicitNulls) formal1 match {
1786-
case OrNull(formal1b) => formal1b.isAnyRef
1787-
case _ => false
1788-
}
1789-
else formal1.isAnyRef
1790-
def formal2IsObject =
1791-
if (ctx.explicitNulls) formal2 match {
1792-
case OrNull(formal2b) => formal2b.isAnyRef
1793-
case _ => false
1794-
}
1795-
else formal2.isAnyRef
1796-
(isSameTypeWhenFrozen(formal1, formal2a)
1797-
|| tp1.isJavaMethod && formal2IsObject && formal1.isAny
1798-
|| tp2.isJavaMethod && formal1IsObject && formal2.isAny
1799-
)
1800-
&& loop(rest1, rest2)
1784+
isSameTypeWhenFrozen(formal1, formal2a) && loop(rest1, rest2)
18011785
case nil =>
18021786
false
18031787
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ object Types {
205205
def isAnyRef(using Context): Boolean = isRef(defn.ObjectClass, skipRefined = false)
206206
def isAnyKind(using Context): Boolean = isRef(defn.AnyKindClass, skipRefined = false)
207207

208+
def isFromJavaObject(using Context): Boolean = typeSymbol eq defn.FromJavaObjectSymbol
209+
208210
/** Does this type refer exactly to class symbol `sym`, instead of to a subclass of `sym`?
209211
* Implemented like `isRef`, but follows more types: all type proxies as well as and- and or-types
210212
*/

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

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -305,10 +305,6 @@ class ClassfileParser(
305305
}
306306
}
307307

308-
/** Map direct references to Object to references to Any */
309-
final def objToAny(tp: Type)(using Context): Type =
310-
if (tp.isDirectRef(defn.ObjectClass) && !ctx.phase.erasedTypes) defn.AnyType else tp
311-
312308
def constantTagToType(tag: Int)(using Context): Type =
313309
(tag: @switch) match {
314310
case BYTE_TAG => defn.ByteType
@@ -356,14 +352,14 @@ class ClassfileParser(
356352
case variance @ ('+' | '-' | '*') =>
357353
index += 1
358354
variance match {
359-
case '+' => objToAny(TypeBounds.upper(sig2type(tparams, skiptvs)))
355+
case '+' => TypeBounds.upper(sig2type(tparams, skiptvs))
360356
case '-' =>
361-
val tp = sig2type(tparams, skiptvs)
362-
// sig2type seems to return AnyClass regardless of the situation:
363-
// we don't want Any as a LOWER bound.
364-
if (tp.isDirectRef(defn.AnyClass)) TypeBounds.empty
365-
else TypeBounds.lower(tp)
366-
case '*' => TypeBounds.empty
357+
val argTp = sig2type(tparams, skiptvs)
358+
// Interpret `sig2type` returning `Any` as "no bounds";
359+
// morally equivalent to TypeBounds.empty, but we're representing Java code, so use FromJavaObjectType as the upper bound
360+
if (argTp.typeSymbol == defn.AnyClass) TypeBounds.upper(defn.FromJavaObjectType)
361+
else TypeBounds(argTp, defn.FromJavaObjectType)
362+
case '*' => TypeBounds.upper(defn.FromJavaObjectType)
367363
}
368364
case _ => sig2type(tparams, skiptvs)
369365
}
@@ -379,7 +375,8 @@ class ClassfileParser(
379375
}
380376

381377
val classSym = classNameToSymbol(subName(c => c == ';' || c == '<'))
382-
var tpe = processClassType(processInner(classSym.typeRef))
378+
val classTpe = if (classSym eq defn.ObjectClass) defn.FromJavaObjectType else classSym.typeRef
379+
var tpe = processClassType(processInner(classTpe))
383380
while (sig(index) == '.') {
384381
accept('.')
385382
val name = subName(c => c == ';' || c == '<' || c == '.').toTypeName
@@ -426,7 +423,7 @@ class ClassfileParser(
426423
// `ElimRepeated` is responsible for correctly erasing this.
427424
defn.RepeatedParamType.appliedTo(elemType)
428425
else
429-
objToAny(sig2type(tparams, skiptvs))
426+
sig2type(tparams, skiptvs)
430427
}
431428

432429
index += 1
@@ -448,7 +445,7 @@ class ClassfileParser(
448445
while (sig(index) == ':') {
449446
index += 1
450447
if (sig(index) != ':') // guard against empty class bound
451-
ts += objToAny(sig2type(tparams, skiptvs))
448+
ts += sig2type(tparams, skiptvs)
452449
}
453450
val bound = if ts.isEmpty then defn.AnyType else ts.reduceLeft(AndType.apply)
454451
TypeBounds.upper(bound)
@@ -497,7 +494,7 @@ class ClassfileParser(
497494
classTParams = tparams
498495
val parents = new ListBuffer[Type]()
499496
while (index < end)
500-
parents += sig2type(tparams, skiptvs = false) // here the variance doesn't matter
497+
parents += sig2type(tparams, skiptvs = false) // here the variance doesn't matter
501498
TempClassInfoType(parents.toList, instanceScope, owner)
502499
}
503500
if (ownTypeParams.isEmpty) tpe else TempPolyType(ownTypeParams, tpe)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ object JavaParsers {
304304
if (in.token == QMARK) {
305305
val offset = in.offset
306306
in.nextToken()
307-
val hi = if (in.token == EXTENDS) { in.nextToken() ; typ() } else EmptyTree
307+
val hi = if (in.token == EXTENDS) { in.nextToken() ; typ() } else javaLangObject()
308308
val lo = if (in.token == SUPER) { in.nextToken() ; typ() } else EmptyTree
309309
atSpan(offset) {
310310
/*
@@ -434,7 +434,7 @@ object JavaParsers {
434434
def typeParam(flags: FlagSet): TypeDef =
435435
atSpan(in.offset) {
436436
val name = identForType()
437-
val hi = if (in.token == EXTENDS) { in.nextToken() ; bound() } else EmptyTree
437+
val hi = if (in.token == EXTENDS) { in.nextToken() ; bound() } else javaLangObject()
438438
TypeDef(name, TypeBoundsTree(EmptyTree, hi)).withMods(Modifiers(flags))
439439
}
440440

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package printing
33

44
import core._
55
import Texts._, Types._, Flags._, Names._, Symbols._, NameOps._, Constants._, Denotations._
6+
import StdNames._
67
import Contexts._
78
import Scopes.Scope, Denotations.Denotation, Annotations.Annotation
89
import StdNames.nme
@@ -89,7 +90,10 @@ class PlainPrinter(_ctx: Context) extends Printer {
8990
|| (sym.name == nme.PACKAGE) // package
9091
)
9192

92-
def nameString(name: Name): String = name.toString
93+
def nameString(name: Name): String =
94+
if (name eq tpnme.FromJavaObject) && !printDebug
95+
then nameString(tpnme.Object)
96+
else name.toString
9397

9498
def toText(name: Name): Text = Str(nameString(name))
9599

@@ -123,11 +127,13 @@ class PlainPrinter(_ctx: Context) extends Printer {
123127
})
124128

125129
/** Direct references to these symbols are printed without their prefix for convenience.
126-
* They are either aliased in scala.Predef or in the scala package object.
130+
* They are either aliased in scala.Predef or in the scala package object, as well as `Object`
127131
*/
128132
private lazy val printWithoutPrefix: Set[Symbol] =
129133
(defn.ScalaPredefModule.termRef.typeAliasMembers
130134
++ defn.ScalaPackageObject.termRef.typeAliasMembers).map(_.info.classSymbol).toSet
135+
+ defn.ObjectClass
136+
+ defn.FromJavaObjectSymbol
131137

132138
def toText(tp: Type): Text = controlled {
133139
homogenize(tp) match {
@@ -267,7 +273,9 @@ class PlainPrinter(_ctx: Context) extends Printer {
267273
simpleNameString(sym) + idString(sym) // + "<" + (if (sym.exists) sym.owner else "") + ">"
268274

269275
def fullNameString(sym: Symbol): String =
270-
if (sym.isRoot || sym == NoSymbol || sym.owner.isEffectiveRoot)
276+
if (sym eq defn.FromJavaObjectSymbol) && !printDebug then
277+
fullNameString(defn.ObjectClass)
278+
else if sym.isRoot || sym == NoSymbol || sym.owner.isEffectiveRoot then
271279
nameString(sym)
272280
else
273281
fullNameString(fullNameOwner(sym)) + "." + nameString(sym)
@@ -365,7 +373,7 @@ class PlainPrinter(_ctx: Context) extends Printer {
365373
" = " ~ toText(tp.alias)
366374
case TypeBounds(lo, hi) =>
367375
(if (lo isRef defn.NothingClass) Text() else " >: " ~ toText(lo))
368-
~ (if hi.isAny then Text() else " <: " ~ toText(hi))
376+
~ (if hi.isAny || (!printDebug && hi.isFromJavaObject) then Text() else " <: " ~ toText(hi))
369377
tparamStr ~ binder
370378
case tp @ ClassInfo(pre, cls, cparents, decls, selfInfo) =>
371379
val preText = toTextLocal(pre)

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
7676
}
7777

7878
override def nameString(name: Name): String =
79-
if (ctx.settings.YdebugNames.value) name.debugString else name.toString
79+
if ctx.settings.YdebugNames.value
80+
then name.debugString
81+
else super.nameString(name)
8082

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

0 commit comments

Comments
 (0)