diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 9591bc5a93f0..eba086e5ad46 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -495,14 +495,22 @@ object desugar { case Select(qual, tpnme.AnyVal) => isScala(qual) case _ => false } + def isScala(tree: Tree): Boolean = tree match { case Ident(nme.scala) => true case Select(Ident(nme.ROOTPKG), nme.scala) => true case _ => false } + def isRecord(tree: Tree): Boolean = tree match { + case Select(Select(Select(Ident(nme.ROOTPKG), nme.java), nme.lang), tpnme.Record) => true + case _ => false + } + def namePos = cdef.sourcePos.withSpan(cdef.nameSpan) + val isJavaRecord = mods.is(JavaDefined) && parents.exists(isRecord) + val isObject = mods.is(Module) val isCaseClass = mods.is(Case) && !isObject val isCaseObject = mods.is(Case) && isObject @@ -769,6 +777,11 @@ object desugar { val companionMembers = defaultGetters ::: enumCases + def tupleApply(params: List[untpd.Tree]): untpd.Apply = { + val fun = Select(Ident(nme.scala), s"${StdNames.str.Tuple}$arity".toTermName) + Apply(fun, params) + } + // The companion object definitions, if a companion is needed, Nil otherwise. // companion definitions include: // 1. If class is a case class case class C[Ts](p1: T1, ..., pN: TN)(moreParams): @@ -801,9 +814,8 @@ object desugar { case vparam :: Nil => Apply(scalaDot(nme.Option), Select(Ident(unapplyParamName), vparam.name)) case vparams => - val tupleApply = Select(Ident(nme.scala), s"Tuple$arity".toTermName) - val members = vparams.map(vparam => Select(Ident(unapplyParamName), vparam.name)) - Apply(scalaDot(nme.Option), Apply(tupleApply, members)) + val members = vparams.map(param => Select(Ident(unapplyParamName), param.name)) + Apply(scalaDot(nme.Option), tupleApply(members)) val hasRepeatedParam = constrVparamss.head.exists { case ValDef(_, tpt, _) => isRepeated(tpt) @@ -832,6 +844,43 @@ object desugar { companionDefs(anyRef, companionMembers) else if isValueClass && !isObject then companionDefs(anyRef, Nil) + else if (isJavaRecord) { + + /** Get the canonical constructor of the Java record. + * + * Java classes have a dummy constructor; see [[JavaParsers.makeTemplate]] for + * more details + */ + def canonicalConstructor(impl: Template): DefDef = { + impl.body.collectFirst { + case ddef: DefDef if ddef.name.isConstructorName && ddef.mods.is(Synthetic) => + ddef + }.get + } + + val constr1 = canonicalConstructor(impl) + val tParams = constr1.leadingTypeParams + val vParams = asTermOnly(constr1.trailingParamss).head + val arity = vParams.length + + val classTypeRef = appliedRef(classTycon) + + val unapplyParam = makeSyntheticParameter(tpt = classTypeRef) + + val unapplyRHS = + if (arity == 0) Literal(Constant(true)) + else Ident(unapplyParam.name) + + val unapplyResTp = if (arity == 0) Literal(Constant(true)) else TypeTree() + + val unapplyMeth = DefDef( + nme.unapply, + joinParams(derivedTparams, (unapplyParam :: Nil) :: Nil), + unapplyResTp, + unapplyRHS + ).withMods(synthetic | Inline) + companionDefs(anyRef, unapplyMeth :: Nil) + } else Nil enumCompanionRef match { diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 3cde29ee3d79..d758f10aefb9 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -974,6 +974,9 @@ class Definitions { @tu lazy val RuntimeTuples_isInstanceOfEmptyTuple: Symbol = RuntimeTuplesModule.requiredMethod("isInstanceOfEmptyTuple") @tu lazy val RuntimeTuples_isInstanceOfNonEmptyTuple: Symbol = RuntimeTuplesModule.requiredMethod("isInstanceOfNonEmptyTuple") + @tu lazy val JavaRecordReflectMirrorTypeRef: TypeRef = requiredClassRef("scala.runtime.JavaRecordMirror") + @tu lazy val JavaRecordReflectMirrorModule: Symbol = requiredModule("scala.runtime.JavaRecordMirror") + @tu lazy val TupledFunctionTypeRef: TypeRef = requiredClassRef("scala.util.TupledFunction") def TupledFunctionClass(using Context): ClassSymbol = TupledFunctionTypeRef.symbol.asClass def RuntimeTupleFunctionsModule(using Context): Symbol = requiredModule("scala.runtime.TupledFunctions") @@ -1636,6 +1639,7 @@ class Definitions { def isAbstractFunctionClass(cls: Symbol): Boolean = isVarArityClass(cls, str.AbstractFunction) def isTupleClass(cls: Symbol): Boolean = isVarArityClass(cls, str.Tuple) def isProductClass(cls: Symbol): Boolean = isVarArityClass(cls, str.Product) + def isJavaRecordClass(cls: Symbol): Boolean = cls.is(JavaDefined) && cls.derivesFrom(JavaRecordClass) def isBoxedUnitClass(cls: Symbol): Boolean = cls.isClass && (cls.owner eq ScalaRuntimePackageClass) && cls.name == tpnme.BoxedUnit diff --git a/compiler/src/dotty/tools/dotc/core/NamerOps.scala b/compiler/src/dotty/tools/dotc/core/NamerOps.scala index 75a135826785..cebe6ed44b14 100644 --- a/compiler/src/dotty/tools/dotc/core/NamerOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NamerOps.scala @@ -82,6 +82,9 @@ object NamerOps: /** The flags of an `apply` method that serves as a constructor proxy */ val ApplyProxyFlags = Synthetic | ConstructorProxy | Inline | Method + /** TODO: It would be nice if this was inline. Probably want an extra flag for `Proxy`? */ + val UnApplyProxyFlags = Synthetic | Method | Inline | ConstructorProxy + /** If this is a reference to a class and the reference has a stable prefix, the reference * otherwise NoType */ @@ -105,6 +108,8 @@ object NamerOps: def complete(denot: SymDenotation)(using Context): Unit = denot.info = constr.info + // This is weird, but possible. + /** Add constructor proxy apply methods to `scope`. Proxies are for constructors * in `cls` and they reside in `modcls`. */ @@ -121,6 +126,24 @@ object NamerOps: scope end addConstructorApplies + def addIdentUnApply(scope: MutableScope, cls: ClassSymbol, modCls: ClassSymbol)(using ctx: Context): scope.type = + def proxy(constr: Symbol): Symbol = + val typeRef = cls.typeRef + newSymbol( + modCls, nme.unapply, + // The modifiers on unapply are essentially the same as on the constructor + UnApplyProxyFlags | (constr.flagsUNSAFE & AccessFlags), + MethodType(typeRef :: Nil, typeRef), + cls.privateWithin, + // TODO: Does this work? Or are we going to run into issues because the type it not the same? + constr.coord + ) + + val decl = cls.info.decls.find(_.isConstructor) + scope.enter(proxy(decl)) + scope + end addIdentUnApply + /** The completer of a constructor companion for class `cls`, where * `modul` is the companion symbol and `modcls` is its class. */ @@ -150,6 +173,7 @@ object NamerOps: newSymbol(tsym.owner, tsym.name.toTermName, ConstructorCompanionFlags | StableRealizable | Method, ExprType(prefix.select(proxy)), coord = tsym.coord) + // TODO: Rename to addSyntheticProxies /** Add all necessary constructor proxy symbols for members of class `cls`. This means: * * - if a member is a class, or type alias, that needs a constructor companion, add one, @@ -172,17 +196,20 @@ object NamerOps: then classConstructorCompanion(mbr).entered case _ => + // TODO: What is this? underlyingStableClassRef(mbr.info.loBound): @unchecked match case ref: TypeRef => val proxy = ref.symbol.registeredCompanion if proxy.is(ConstructorProxy) && !memberExists(cls, mbr.name.toTermName) then typeConstructorCompanion(mbr, ref.prefix, proxy).entered - if cls.is(Module) - && needsConstructorProxies(cls.linkedClass) - && !memberExists(cls, nme.apply) - then - addConstructorApplies(cls.info.decls.openForMutations, cls.linkedClass.asClass, cls) + if cls.is(Module) then + if(needsConstructorProxies(cls.linkedClass) && !memberExists(cls, nme.apply)) then + addConstructorApplies(cls.info.decls.openForMutations, cls.linkedClass.asClass, cls) + + if(defn.isJavaRecordClass(cls.linkedClass) && !memberExists(cls, nme.unapply)) then + addIdentUnApply(cls.info.decls.openForMutations, cls.linkedClass.asClass, cls) + end addConstructorProxies /** Turn `modul` into a constructor companion for class `cls` */ diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index a2e78add1338..176104bf0de3 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -273,6 +273,7 @@ object StdNames { final val MethodParametersATTR: N = "MethodParameters" final val LineNumberTableATTR: N = "LineNumberTable" final val LocalVariableTableATTR: N = "LocalVariableTable" + final val RecordATTR: N = "Record" // Introduced in JEP-395 final val RuntimeVisibleAnnotationATTR: N = "RuntimeVisibleAnnotations" // RetentionPolicy.RUNTIME final val RuntimeInvisibleAnnotationATTR: N = "RuntimeInvisibleAnnotations" // RetentionPolicy.CLASS final val RuntimeParamAnnotationATTR: N = "RuntimeVisibleParameterAnnotations" // RetentionPolicy.RUNTIME (annotations on parameters) diff --git a/compiler/src/dotty/tools/dotc/core/SymUtils.scala b/compiler/src/dotty/tools/dotc/core/SymUtils.scala index 65634241b790..462550271aa8 100644 --- a/compiler/src/dotty/tools/dotc/core/SymUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/SymUtils.scala @@ -99,13 +99,14 @@ class SymUtils: def canAccessCtor: Boolean = def isAccessible(sym: Symbol): Boolean = ctx.owner.isContainedIn(sym) def isSub(sym: Symbol): Boolean = ctx.owner.ownersIterator.exists(_.derivesFrom(sym)) - val ctor = self.primaryConstructor + val ctor = if defn.isJavaRecordClass(self) then self.javaCanonicalConstructor else self.primaryConstructor (!ctor.isOneOf(Private | Protected) || isSub(self)) // we cant access the ctor because we do not extend cls && (!ctor.privateWithin.exists || isAccessible(ctor.privateWithin)) // check scope is compatible def companionMirror = self.useCompanionAsProductMirror - if (!self.is(CaseClass)) "it is not a case class" + + if (!(self.is(CaseClass) || defn.isJavaRecordClass(self))) "it is not a case class or record class" else if (self.is(Abstract)) "it is an abstract class" else if (self.primaryConstructor.info.paramInfoss.length != 1) "it takes more than one parameter list" else if self.isDerivedValueClass then "it is a value class" @@ -146,7 +147,7 @@ class SymUtils: && (!self.is(Method) || self.is(Accessor)) def useCompanionAsProductMirror(using Context): Boolean = - self.linkedClass.exists && !self.is(Scala2x) && !self.linkedClass.is(Case) + self.linkedClass.exists && !self.is(Scala2x) && !self.linkedClass.is(Case) && !defn.isJavaRecordClass(self) def useCompanionAsSumMirror(using Context): Boolean = def companionExtendsSum(using Context): Boolean = @@ -249,6 +250,16 @@ class SymUtils: def caseAccessors(using Context): List[Symbol] = self.info.decls.filter(_.is(CaseAccessor)) + // TODO: I'm convinced that we need to introduce a flag to get the canonical constructor. + // we should also check whether the names are erased in the ctor. If not, we should + // be able to infer the components directly from the constructor. + def javaCanonicalConstructor(using Context): Symbol = + self.info.decls.filter(_.isConstructor).tail.head + + // TODO: Check if `Synthetic` is stamped properly + def javaRecordComponents(using Context): List[Symbol] = + self.info.decls.filter(_.is(ParamAccessor)) + def getter(using Context): Symbol = if (self.isGetter) self else accessorNamed(self.asTerm.name.getterName) diff --git a/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala b/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala index 75c610b29140..96137aed73a9 100644 --- a/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala +++ b/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala @@ -366,6 +366,7 @@ abstract class SymbolLoader extends LazyType { self => if !denot.isCompleted && !denot.completer.isInstanceOf[SymbolLoaders.SecondCompleter] then if denot.is(ModuleClass) && NamerOps.needsConstructorProxies(other) then + // TODO: What to do here? NamerOps.makeConstructorCompanion(denot.sourceModule.asTerm, other.asClass) denot.resetFlag(Touched) else diff --git a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala index 894d430fe54b..303a00f7e2ba 100644 --- a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala +++ b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala @@ -895,13 +895,14 @@ class ClassfileParser( var exceptions: List[NameOrString] = Nil var annotations: List[Annotation] = Nil var namedParams: Map[Int, TermName] = Map.empty + def complete(tp: Type, isVarargs: Boolean = false)(using Context): Type = { val updatedType = if sig == null then tp else { val newType = sigToType(sig, sym, isVarargs) - if (ctx.debug && ctx.verbose) - println("" + sym + "; signature = " + sig + " type = " + newType) + if (ctx.verbose) + report.debuglog("" + sym + "; signature = " + sig + " type = " + newType) newType } @@ -911,7 +912,7 @@ class ClassfileParser( if ct != null then ConstantType(ct) else updatedType else updatedType - annotations.foreach(annot => sym.addAnnotation(annot)) + annotations.foreach(sym.addAnnotation) exceptions.foreach { ex => val cls = getClassSymbol(ex.name) @@ -995,11 +996,38 @@ class ClassfileParser( report.log(s"$sym in ${sym.owner} is a java 8+ default method.") } + // https://docs.oracle.com/javase/specs/jvms/se15/preview/specs/records-jvms.html + case tpnme.RecordATTR => + // Each member is /not/ sythetic on purpose + parseRecord() case _ => } in.bp = end } + /** + * Parse the `Record` attribute. + * + * The `Record` attribute contains the _name_ and _descriptor_ for + * each component within the `Record` in the order of the canonical + * constructor. + */ + def parseRecord(): Unit = { + val componentsCount = in.nextChar.toInt + val components = for (i <- 0 until componentsCount) yield + val nameIndex = in.nextChar.toInt + val descriptorIndex = in.nextChar.toInt + + val name = pool.getName(nameIndex) + val descriptor = pool.getName(descriptorIndex) + + instanceScope.lookup(name.name).setFlag(Flags.ParamAccessor) + + // TODO: Double /where/ we want these attributes to sit. + skipAttributes() + (name, name) + } + /** * Parse the "Exceptions" attribute which denotes the exceptions * thrown by a method. diff --git a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala index e98ff6c9d66d..67d3e070d315 100644 --- a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala @@ -852,10 +852,11 @@ object JavaParsers { fieldsByName -= name end for + // The `Synthetic` flag here is only used by the `Namer` to evict overriden symbols during mixed-compilation val accessors = (for (name, (tpt, annots)) <- fieldsByName yield DefDef(name, List(Nil), tpt, unimplementedExpr) - .withMods(Modifiers(Flags.JavaDefined | Flags.Method | Flags.Synthetic)) + .withMods(Modifiers(Flags.JavaDefined | Flags.Method | Flags.ParamAccessor | Flags.Synthetic)) ).toList // generate the canonical constructor diff --git a/compiler/src/dotty/tools/dotc/transform/Inlining.scala b/compiler/src/dotty/tools/dotc/transform/Inlining.scala index 907fe948ac30..076500d84ff8 100644 --- a/compiler/src/dotty/tools/dotc/transform/Inlining.scala +++ b/compiler/src/dotty/tools/dotc/transform/Inlining.scala @@ -5,6 +5,8 @@ import core.* import Flags.* import Contexts.* import Symbols.* +import StdNames.* +import Decorators.* import dotty.tools.dotc.ast.tpd import dotty.tools.dotc.ast.Trees.* @@ -14,6 +16,8 @@ import dotty.tools.dotc.ast.TreeMapWithImplicits import dotty.tools.dotc.core.DenotTransformers.IdentityDenotTransformer import dotty.tools.dotc.staging.StagingLevel +import config.Printers.inlining + import scala.collection.mutable.ListBuffer /** Inlines all calls to inline methods that are not in an inline method or a quote */ @@ -55,8 +59,9 @@ class Inlining extends MacroTransform, IdentityDenotTransformer { } def newTransformer(using Context): Transformer = new Transformer { - override def transform(tree: tpd.Tree)(using Context): tpd.Tree = + override def transform(tree: tpd.Tree)(using Context): tpd.Tree = { new InliningTreeMap().transform(tree) + } } private class InliningTreeMap extends TreeMapWithImplicits { @@ -88,6 +93,11 @@ class Inlining extends MacroTransform, IdentityDenotTransformer { flatTree(trees2) else super.transform(tree) + case Apply(fun, args) if fun.symbol.name == nme.unapply && fun.symbol.is(ConstructorProxy) => + // TODO: Add for using it as a value + val tree1 = args.head + inlining.println(i"reducing unapply proxy: $tree -> $tree1") + tree1 case _: Typed | _: Block => super.transform(tree) case _ if Inlines.needsInlining(tree) => diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index bed29a122399..8dd3424e450f 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -343,8 +343,15 @@ object PatternMatcher { .select(defn.RuntimeTuples_apply) .appliedTo(receiver, Literal(Constant(i))) + def resultTypeSym = unapp.symbol.info.resultType.typeSymbol + + // TODO: Check Scala -> Java, erased? + def isJavaRecordUnapply(sym: Symbol) = defn.isJavaRecordClass(resultTypeSym) + def tupleSel(sym: Symbol) = ref(scrutinee).select(sym) + def recordSel(sym: Symbol) = tupleSel(sym).appliedToTermArgs(Nil) + + // TODO: Move this to the correct location if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length) - def tupleSel(sym: Symbol) = ref(scrutinee).select(sym) val isGenericTuple = defn.isTupleClass(caseClass) && !defn.isTupleNType(tree.tpe match { case tp: OrType => tp.join case tp => tp }) // widen even hard unions, to see if it's a union of tuples val components = if isGenericTuple then caseAccessors.indices.toList.map(tupleApp(_, ref(scrutinee))) else caseAccessors.map(tupleSel) @@ -369,7 +376,10 @@ object PatternMatcher { else if unappResult.info <:< defn.NonEmptyTupleTypeRef then val components = (0 until foldApplyTupleType(unappResult.denot.info).length).toList.map(tupleApp(_, ref(unappResult))) matchArgsPlan(components, args, onSuccess) - else { + else if (isJavaRecordUnapply(unapp.symbol.owner)) { + val components = resultTypeSym.javaRecordComponents.map(recordSel) + matchArgsPlan(components, args, onSuccess) + } else { assert(isGetMatch(unapp.tpe)) val argsPlan = { val get = ref(unappResult).select(nme.get, _.info.isParameterless) diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 63f6af2beb86..b7b62ed6c8a0 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -260,8 +260,9 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => } } + // TODO: Add this check back in at some point. def checkNoConstructorProxy(tree: Tree)(using Context): Unit = - if tree.symbol.is(ConstructorProxy) then + if tree.symbol.is(ConstructorProxy) && tree.symbol.name != nme.unapply then report.error(em"constructor proxy ${tree.symbol} cannot be used as a value", tree.srcPos) def checkStableSelection(tree: Tree)(using Context): Unit = @@ -307,6 +308,7 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => checkNoConstructorProxy(tree) transformSelect(tree, Nil) case tree: Apply => + // println(tree.show) val methType = tree.fun.tpe.widen.asInstanceOf[MethodType] val app = if (methType.hasErasedParams) diff --git a/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala b/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala index 6d2aedb9b47b..e3133af7b7fd 100644 --- a/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala +++ b/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala @@ -521,6 +521,7 @@ class SyntheticMembers(thisPhase: DenotTransformer) { def computeFromCaseClass: (Type, List[Type]) = val (baseRef, baseInfo) = val rawRef = caseClass.typeRef + // TODO: HERE!! val rawInfo = caseClass.primaryConstructor.info optInfo match case Some(info) => diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 452c6d197310..8548ca7ffeb9 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -49,6 +49,10 @@ object Applications { ref.info.widenExpr.annotatedToRepeated } + // TODO: Error here + def isJavaRecordMatch(tp: Type, numArgs: Int, errorPos: SrcPos = NoSourcePosition)(using Context): Boolean = + defn.isJavaRecordClass(tp.typeSymbol) + /** Does `tp` fit the "product match" conditions as an unapply result type * for a pattern with `numArgs` subpatterns? * This is the case if `tp` has members `_1` to `_N` where `N == numArgs`. @@ -108,6 +112,20 @@ object Applications { if (isValid) elemTp else NoType } + def javaRecordComponentTypes(tp: Type, errorPos: SrcPos)(using Context): List[Type] = { + + val params = tp.typeSymbol.javaRecordComponents.map(_.info.resultType) + + tp match + case tp: AppliedType => + val argsIter = tp.args.iterator + for (param <- params) yield param match + case param if param.typeSymbol.isTypeParam => argsIter.next() + case param => param + case _ => params + + } + def productSelectorTypes(tp: Type, errorPos: SrcPos)(using Context): List[Type] = { val sels = for (n <- Iterator.from(0)) yield extractorMemberType(tp, nme.selectorName(n), errorPos) sels.takeWhile(_.exists).toList @@ -192,6 +210,8 @@ object Applications { // which is better than the message in `fail`. else if unapplyResult.derivesFrom(defn.NonEmptyTupleClass) then foldApplyTupleType(unapplyResult) + else if (isJavaRecordMatch(unapplyResult, args.length, pos)) then + javaRecordComponentTypes(unapplyResult, pos) else fail } } diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 03ff6e168666..6e02b005ac7b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -880,9 +880,6 @@ class Namer { typer: Typer => */ private def invalidateIfClashingSynthetic(denot: SymDenotation): Unit = - def isJavaRecord(owner: Symbol) = - owner.is(JavaDefined) && owner.derivesFrom(defn.JavaRecordClass) - def isCaseClassOrCompanion(owner: Symbol) = owner.isClass && { if (owner.is(Module)) owner.linkedClass.is(CaseClass) @@ -907,7 +904,7 @@ class Namer { typer: Typer => ) || // remove synthetic constructor or method of a java Record if it clashes with a non-synthetic constructor - (isJavaRecord(denot.owner) + (defn.isJavaRecordClass(denot.owner) && denot.is(Method) && denot.owner.unforcedDecls.lookupAll(denot.name).exists(c => c != denot.symbol && c.info.matches(denot.info)) ) diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index c94724faf4d4..1683a19b1d78 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -407,10 +407,19 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): def newTupleMirror(arity: Int): Tree = New(defn.RuntimeTupleMirrorTypeRef, Literal(Constant(arity)) :: Nil) + def newJavaRecordReflectMirror(tpe: Type) = + ref(defn.JavaRecordReflectMirrorModule) + .select(nme.apply) + .appliedToType(tpe) + .appliedTo(clsOf(tpe)) + def makeProductMirror(pre: Type, cls: Symbol, tps: Option[List[Type]]): TreeWithErrors = - val accessors = cls.caseAccessors + val (accessors, accessorType) = if (defn.isJavaRecordClass(cls)) then + (cls.javaRecordComponents, (t: Type) => t.resultType) + else + (cls.caseAccessors, identity[Type]) val elemLabels = accessors.map(acc => ConstantType(Constant(acc.name.toString))) - val typeElems = tps.getOrElse(accessors.map(mirroredType.resultType.memberInfo(_).widenExpr)) + val typeElems = tps.getOrElse(accessors.map(accessor => accessorType(mirroredType.resultType.memberInfo(accessor)).widenExpr)) val nestedPairs = TypeOps.nestedPairs(typeElems) val (monoType, elemsType) = mirroredType match case mirroredType: HKTypeLambda => @@ -427,6 +436,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): } val mirrorRef = if cls.useCompanionAsProductMirror then companionPath(mirroredType, span) + else if defn.isJavaRecordClass(cls) then newJavaRecordReflectMirror(cls.typeRef) else if defn.isTupleClass(cls) then newTupleMirror(typeElems.size) // TODO: cls == defn.PairClass when > 22 else anonymousMirror(monoType, MirrorImpl.OfProduct(pre), span) withNoErrors(mirrorRef.cast(mirrorType).withSpan(span)) @@ -458,6 +468,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): val reason = s"it reduces to a tuple with arity $arity, expected arity <= $maxArity" withErrors(i"${defn.PairClass} is not a generic product because $reason") case MirrorSource.ClassSymbol(pre, cls) => + if cls.isGenericProduct then if ctx.runZincPhases then // The mirror should be resynthesized if the constructor of the diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 64c2bd3b8807..bee2eb6315f4 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -26,6 +26,8 @@ class CompilationTests { import CompilationTests._ import CompilationTest.aggregateTests + def `isJava16+`: Boolean = scala.util.Properties.isJavaAtLeast("16") + // Positive tests ------------------------------------------------------------ @Test def pos: Unit = { @@ -50,8 +52,12 @@ class CompilationTests { else Nil ) - if scala.util.Properties.isJavaAtLeast("16") then - tests ::= compileFilesInDir("tests/pos-java16+", defaultOptions.and("-Ysafe-init")) + if `isJava16+` then + // We need to make sure that the ingested java class files are compiled correctly as well! + tests ++= List( + compileFilesInDir("tests/pos-java16+", defaultOptions.and("-Ysafe-init")), + compileFilesInDir("tests/pos-java16+", defaultOptions.and("-Ysafe-init").compileJavaThenScala), + ) aggregateTests(tests*).checkCompile() } @@ -155,13 +161,19 @@ class CompilationTests { @Test def runAll: Unit = { implicit val testGroup: TestGroup = TestGroup("runAll") - aggregateTests( + var tests = List( compileFilesInDir("tests/run", defaultOptions.and("-Ysafe-init")), compileFilesInDir("tests/run-deep-subtype", allowDeepSubtypes), compileFilesInDir("tests/run-custom-args/captures", allowDeepSubtypes.and("-language:experimental.captureChecking")), // Run tests for legacy lazy vals. compileFilesInDir("tests/run", defaultOptions.and("-Ysafe-init", "-Ylegacy-lazy-vals", "-Ycheck-constraint-deps"), FileFilter.include(TestSources.runLazyValsAllowlist)), - ).checkRuns() + ) + + if `isJava16+` then + tests ::= compileFilesInDir("tests/run-java16+", defaultOptions.and("-Ysafe-init")) + tests ::= compileFilesInDir("tests/pos-java16+", defaultOptions.and("-Ysafe-init").compileJavaThenScala) + + aggregateTests(tests*).checkRuns() } // Generic java signatures tests --------------------------------------------- diff --git a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala index 59d5d3d542fd..3ae72e2b67e2 100644 --- a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala +++ b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala @@ -543,9 +543,29 @@ trait ParallelTesting extends RunnerOrchestration { self => val allArgs = flags.all if testIsFiltered then + + import TestFlags.CompilationMode + // If a test contains a Java file that cannot be parsed by Dotty's Java source parser, its // name must contain the string "JAVA_ONLY". - val dottyFiles = files.filterNot(_.getName.contains("JAVA_ONLY")).map(_.getPath) + val (dottyFiles, allArgs) = flags.compilationMode match + // case CompilationMode.Mixed => files.filterNot(_.getName.contains("JAVA_ONLY")).map(_.getPath) + case _ => + val (javaFiles, dottyFiles) = files.partition(_.getName.endsWith(".java")) + val javaErrors = compileWithJavac(javaFiles.map(_.getPath)) + if (javaErrors.isDefined) { + echo(s"\njava compilation failed: \n${ javaErrors.get }") + fail(failure = JavaCompilationFailure(javaErrors.get)) + } + // val classFiles = javaFiles.map { file => + // val name = file.getName + // val className = name.substring(0, name.length - ".java".length) + ".class" + // file.toPath.resolveSibling(className) + // } + // TODO: More robust? + val classPath = javaFiles.head.toPath.getParent.toString + (dottyFiles.map(_.getPath), flags.withClasspath(classPath).all) + driver.process(allArgs ++ dottyFiles, reporter = reporter) // todo a better mechanism than ONLY. test: -scala-only? diff --git a/compiler/test/dotty/tools/vulpix/TestFlags.scala b/compiler/test/dotty/tools/vulpix/TestFlags.scala index acf1c9ae167c..3595ab51a7c8 100644 --- a/compiler/test/dotty/tools/vulpix/TestFlags.scala +++ b/compiler/test/dotty/tools/vulpix/TestFlags.scala @@ -8,7 +8,9 @@ final case class TestFlags( defaultClassPath: String, runClassPath: String, // class path that is used when running `run` tests (not compiling) options: Array[String], - javacOptions: Array[String]) { + javacOptions: Array[String], + compilationMode: TestFlags.CompilationMode = TestFlags.CompilationMode.Mixed +) { def and(flags: String*): TestFlags = TestFlags(defaultClassPath, runClassPath, options ++ flags, javacOptions) @@ -19,6 +21,9 @@ final case class TestFlags( def withClasspath(classPath: String): TestFlags = TestFlags(s"$defaultClassPath${JFile.pathSeparator}$classPath", runClassPath, options, javacOptions) + def compileJavaThenScala: TestFlags = + TestFlags(defaultClassPath, runClassPath, options, javacOptions, TestFlags.CompilationMode.JavaThenScala) + def withRunClasspath(classPath: String): TestFlags = TestFlags(defaultClassPath, s"$runClassPath${JFile.pathSeparator}$classPath", options, javacOptions) @@ -54,5 +59,10 @@ final case class TestFlags( } object TestFlags { + + enum CompilationMode { + case Mixed, JavaThenScala + } + def apply(classPath: String, flags: Array[String]): TestFlags = TestFlags(classPath, classPath, flags, Array.empty) } diff --git a/library/src/scala/runtime/JavaRecordMirror.scala b/library/src/scala/runtime/JavaRecordMirror.scala new file mode 100644 index 000000000000..1beb26a5e31c --- /dev/null +++ b/library/src/scala/runtime/JavaRecordMirror.scala @@ -0,0 +1,34 @@ +package scala.runtime + +import java.lang.Record +import java.lang.reflect.Constructor +import scala.reflect.ClassTag + +// TODO: Rename to JavaRecordReflectMirror +object JavaRecordMirror: + + def apply[T <: Record](clazz: Class[T]): JavaRecordMirror[T] = + val components = clazz.getRecordComponents.nn + val constructorTypes = components.map(_.nn.getType.nn) + val constr = clazz.getDeclaredConstructor(constructorTypes*).nn + new JavaRecordMirror(components.length, constr) + + def of[T <: Record : ClassTag]: JavaRecordMirror[T] = + JavaRecordMirror(summon[ClassTag[T]].runtimeClass.asInstanceOf[Class[T]]) + +// TODO: Is a constructor serializable? +final class JavaRecordMirror[T] private(arity: Int, constr: Constructor[T]) extends scala.deriving.Mirror.Product with Serializable: + + override type MirroredMonoType <: Record + + final def fromProduct(product: Product): MirroredMonoType = + if product.productArity != arity then + throw IllegalArgumentException(s"expected Product with $arity elements, got ${product.productArity}") + else + // TODO: Check this byte code, we want to unroll to give a happy medium between JIT'ing and having tons of extra classes + val t = arity match + case 0 => constr.newInstance() + case 1 => constr.newInstance(product.productElement(0)) + case 2 => constr.newInstance(product.productElement(0), product.productElement(1)) + + t.nn.asInstanceOf[MirroredMonoType] diff --git a/tests/neg/mirror-synthesis-errors.check b/tests/neg/mirror-synthesis-errors.check index 92ce2118e66a..f3a2a04561c2 100644 --- a/tests/neg/mirror-synthesis-errors.check +++ b/tests/neg/mirror-synthesis-errors.check @@ -2,19 +2,19 @@ 21 |val testA = summon[Mirror.Of[A]] // error: Not a sealed trait | ^ |No given instance of type scala.deriving.Mirror.Of[A] was found for parameter x of method summon in object Predef. Failed to synthesize an instance of type scala.deriving.Mirror.Of[A]: - | * trait A is not a generic product because it is not a case class + | * trait A is not a generic product because it is not a case class or record class | * trait A is not a generic sum because it is not a sealed trait -- [E172] Type Error: tests/neg/mirror-synthesis-errors.scala:22:32 ---------------------------------------------------- 22 |val testC = summon[Mirror.Of[C]] // error: Does not have subclasses | ^ |No given instance of type scala.deriving.Mirror.Of[C] was found for parameter x of method summon in object Predef. Failed to synthesize an instance of type scala.deriving.Mirror.Of[C]: - | * trait C is not a generic product because it is not a case class + | * trait C is not a generic product because it is not a case class or record class | * trait C is not a generic sum because it does not have subclasses -- [E172] Type Error: tests/neg/mirror-synthesis-errors.scala:23:32 ---------------------------------------------------- 23 |val testD = summon[Mirror.Of[D]] // error: child SubD takes more than one parameter list | ^ |No given instance of type scala.deriving.Mirror.Of[D] was found for parameter x of method summon in object Predef. Failed to synthesize an instance of type scala.deriving.Mirror.Of[D]: - | * class D is not a generic product because it is not a case class + | * class D is not a generic product because it is not a case class or record class | * class D is not a generic sum because its child class SubD is not a generic product because it takes more than one parameter list -- [E172] Type Error: tests/neg/mirror-synthesis-errors.scala:24:38 ---------------------------------------------------- 24 |val testSubD = summon[Mirror.Of[SubD]] // error: takes more than one parameter list @@ -26,17 +26,17 @@ 25 |val testE = summon[Mirror.Of[E]] // error: Not an abstract class | ^ |No given instance of type scala.deriving.Mirror.Of[E] was found for parameter x of method summon in object Predef. Failed to synthesize an instance of type scala.deriving.Mirror.Of[E]: - | * class E is not a generic product because it is not a case class + | * class E is not a generic product because it is not a case class or record class | * class E is not a generic sum because it is not an abstract class -- [E172] Type Error: tests/neg/mirror-synthesis-errors.scala:26:32 ---------------------------------------------------- 26 |val testF = summon[Mirror.Of[F]] // error: No children | ^ |No given instance of type scala.deriving.Mirror.Of[F] was found for parameter x of method summon in object Predef. Failed to synthesize an instance of type scala.deriving.Mirror.Of[F]: - | * trait F is not a generic product because it is not a case class + | * trait F is not a generic product because it is not a case class or record class | * trait F is not a generic sum because it does not have subclasses -- [E172] Type Error: tests/neg/mirror-synthesis-errors.scala:27:36 ---------------------------------------------------- 27 |val testG = summon[Mirror.Of[Foo.G]] // error: Has anonymous subclasses | ^ |No given instance of type scala.deriving.Mirror.Of[Foo.G] was found for parameter x of method summon in object Predef. Failed to synthesize an instance of type scala.deriving.Mirror.Of[Foo.G]: - | * trait G is not a generic product because it is not a case class + | * trait G is not a generic product because it is not a case class or record class | * trait G is not a generic sum because it has anonymous or inaccessible subclasses diff --git a/tests/pos-java16+/java-records-java-then-scala/FromScala.scala b/tests/pos-java16+/java-records-java-then-scala/FromScala.scala new file mode 100644 index 000000000000..846e380c5eee --- /dev/null +++ b/tests/pos-java16+/java-records-java-then-scala/FromScala.scala @@ -0,0 +1,6 @@ +object C: + + def useR0: Unit = + val R1(s) = R1("asd") + // r match: + // case R1(s) => s diff --git a/tests/pos-java16+/java-records-java-then-scala/R0.java b/tests/pos-java16+/java-records-java-then-scala/R0.java new file mode 100644 index 000000000000..ad43a2b22673 --- /dev/null +++ b/tests/pos-java16+/java-records-java-then-scala/R0.java @@ -0,0 +1 @@ +public record R0() {} diff --git a/tests/pos-java16+/java-records-java-then-scala/R1.java b/tests/pos-java16+/java-records-java-then-scala/R1.java new file mode 100644 index 000000000000..8df3387aa8dc --- /dev/null +++ b/tests/pos-java16+/java-records-java-then-scala/R1.java @@ -0,0 +1 @@ +public record R1(String s) {} diff --git a/tests/pos-java16+/java-records-mirror/FromScala.scala b/tests/pos-java16+/java-records-mirror/FromScala.scala new file mode 100644 index 000000000000..81d3d502b536 --- /dev/null +++ b/tests/pos-java16+/java-records-mirror/FromScala.scala @@ -0,0 +1,6 @@ +import scala.deriving.Mirror + +object C: + def constructR2: R2 = + val mirror = summon[Mirror.ProductOf[R2]] + mirror.fromTuple((3, "asd")) diff --git a/tests/pos-java16+/java-records-mirror/R2.java b/tests/pos-java16+/java-records-mirror/R2.java new file mode 100644 index 000000000000..9ea613fd1ca9 --- /dev/null +++ b/tests/pos-java16+/java-records-mirror/R2.java @@ -0,0 +1 @@ +public record R2(int i, String s) {} diff --git a/tests/pos-java16+/java-records-patmatch/FromScala.scala b/tests/pos-java16+/java-records-patmatch/FromScala.scala new file mode 100644 index 000000000000..c66be4114437 --- /dev/null +++ b/tests/pos-java16+/java-records-patmatch/FromScala.scala @@ -0,0 +1,31 @@ +object C: + + def useR0: Unit = + val r = R0() + + // unapply in valdef + val R0() = r + + // unapply in patmatch + r match { + case R0() => + } + + + def useR1: Int = + val r = R1(1, "foo") + + // unapply in valdef + val R1(i, _) = r + val a: Int = i + + // unapply in patmatch + r match { + case R1(i, _) => i + } + + def useR2: String = + val r = R2("asd") + r match { + case R2(s) => s + } diff --git a/tests/pos-java16+/java-records-patmatch/R0.java b/tests/pos-java16+/java-records-patmatch/R0.java new file mode 100644 index 000000000000..ad43a2b22673 --- /dev/null +++ b/tests/pos-java16+/java-records-patmatch/R0.java @@ -0,0 +1 @@ +public record R0() {} diff --git a/tests/pos-java16+/java-records-patmatch/R1.java b/tests/pos-java16+/java-records-patmatch/R1.java new file mode 100644 index 000000000000..2857939f9df3 --- /dev/null +++ b/tests/pos-java16+/java-records-patmatch/R1.java @@ -0,0 +1 @@ +public record R1(int i, String s) {} diff --git a/tests/pos-java16+/java-records-patmatch/R2.java b/tests/pos-java16+/java-records-patmatch/R2.java new file mode 100644 index 000000000000..865ff94dbe06 --- /dev/null +++ b/tests/pos-java16+/java-records-patmatch/R2.java @@ -0,0 +1 @@ +public record R2(T t) {} diff --git a/tests/run-java16+/java-records/R0.java b/tests/run-java16+/java-records/R0.java new file mode 100644 index 000000000000..ad43a2b22673 --- /dev/null +++ b/tests/run-java16+/java-records/R0.java @@ -0,0 +1 @@ +public record R0() {} diff --git a/tests/run-java16+/java-records/R1.java b/tests/run-java16+/java-records/R1.java new file mode 100644 index 000000000000..40837040d659 --- /dev/null +++ b/tests/run-java16+/java-records/R1.java @@ -0,0 +1 @@ +public record R1(int i) {} diff --git a/tests/run-java16+/java-records/R2.java b/tests/run-java16+/java-records/R2.java new file mode 100644 index 000000000000..3c62909fe96d --- /dev/null +++ b/tests/run-java16+/java-records/R2.java @@ -0,0 +1 @@ +public record R2(T t, int i) {} diff --git a/tests/run-java16+/java-records/R3.java b/tests/run-java16+/java-records/R3.java new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/run-java16+/java-records/Test.scala b/tests/run-java16+/java-records/Test.scala new file mode 100644 index 000000000000..780832621732 --- /dev/null +++ b/tests/run-java16+/java-records/Test.scala @@ -0,0 +1,21 @@ +// scalajs: --skip + +object Test: + def main(args: Array[String]): Unit = + val r0 = R0() + r0 match + case R0() => + + val r1 = R1(42) + r1 match + case R1(i) => assert(i == 42) + + val R1(i) = r1 + assert(i == 42) + + val r2 = R2("foo", 9) + val R2(s, _) = r2 + assert(s == "foo") + + r2 match + case R2(_, i) => assert(i == 9)