From 670664f449a3f2fe4e7ef310589b9440fe3c53d0 Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Wed, 31 Jan 2024 15:38:57 +0000 Subject: [PATCH 01/13] Add support for Java records in patterns. For each Java record, we create a synthetic `unapply` method returning a `Tuple` on the companion object. --- .../src/dotty/tools/dotc/ast/Desugar.scala | 58 ++++++++++++++++++- .../java-records-patmatch/FromScala.scala | 25 ++++++++ .../pos-java16+/java-records-patmatch/R0.java | 1 + .../pos-java16+/java-records-patmatch/R1.java | 1 + 4 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 tests/pos-java16+/java-records-patmatch/FromScala.scala create mode 100644 tests/pos-java16+/java-records-patmatch/R0.java create mode 100644 tests/pos-java16+/java-records-patmatch/R1.java diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 9591bc5a93f0..8d587cfe1888 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,46 @@ 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 tupleApply( + vParams.map( + param => Select(Ident(unapplyParam.name), param.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/tests/pos-java16+/java-records-patmatch/FromScala.scala b/tests/pos-java16+/java-records-patmatch/FromScala.scala new file mode 100644 index 000000000000..f7f0d9687522 --- /dev/null +++ b/tests/pos-java16+/java-records-patmatch/FromScala.scala @@ -0,0 +1,25 @@ +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 + } 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) {} From 89cf8373ee0f3741f6cc43116c951d21736335da Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Wed, 31 Jan 2024 21:58:18 +0000 Subject: [PATCH 02/13] Add extra regression test for generic record. --- tests/pos-java16+/java-records-patmatch/FromScala.scala | 6 ++++++ tests/pos-java16+/java-records-patmatch/R2.java | 1 + 2 files changed, 7 insertions(+) create mode 100644 tests/pos-java16+/java-records-patmatch/R2.java diff --git a/tests/pos-java16+/java-records-patmatch/FromScala.scala b/tests/pos-java16+/java-records-patmatch/FromScala.scala index f7f0d9687522..c66be4114437 100644 --- a/tests/pos-java16+/java-records-patmatch/FromScala.scala +++ b/tests/pos-java16+/java-records-patmatch/FromScala.scala @@ -23,3 +23,9 @@ object C: 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/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) {} From 24343bff48b10da8967e844b15b2b5949913186b Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Wed, 7 Feb 2024 16:34:06 +0000 Subject: [PATCH 03/13] Check if run tests are working on the CI. --- .../test/dotty/tools/dotc/CompilationTests.scala | 13 ++++++++++--- tests/run-java16+/java-records/R1.java | 1 + tests/run-java16+/java-records/Test.scala | 10 ++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 tests/run-java16+/java-records/R1.java create mode 100644 tests/run-java16+/java-records/Test.scala diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 64c2bd3b8807..3eae946b18b2 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,7 +52,7 @@ class CompilationTests { else Nil ) - if scala.util.Properties.isJavaAtLeast("16") then + if `isJava16+` then tests ::= compileFilesInDir("tests/pos-java16+", defaultOptions.and("-Ysafe-init")) aggregateTests(tests*).checkCompile() @@ -155,13 +157,18 @@ 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")) + + aggregateTests(tests*).checkRuns() } // Generic java signatures tests --------------------------------------------- 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/Test.scala b/tests/run-java16+/java-records/Test.scala new file mode 100644 index 000000000000..dd832fb6eb19 --- /dev/null +++ b/tests/run-java16+/java-records/Test.scala @@ -0,0 +1,10 @@ +// scalajs: --skip + +object Test { + def main(args: Array[String]): Unit = { + val r1 = R1(42) + r1 match { + case R1(i) => assert(i == 42) + } + } +} From 406c039ef35dbfb9e0ea0708e47ef2f313c47588 Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Wed, 7 Feb 2024 23:01:06 +0000 Subject: [PATCH 04/13] Add 0 arity test --- tests/run-java16+/java-records/R0.java | 1 + tests/run-java16+/java-records/Test.scala | 16 ++++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 tests/run-java16+/java-records/R0.java 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/Test.scala b/tests/run-java16+/java-records/Test.scala index dd832fb6eb19..df92c9a8cb31 100644 --- a/tests/run-java16+/java-records/Test.scala +++ b/tests/run-java16+/java-records/Test.scala @@ -1,10 +1,14 @@ // scalajs: --skip -object Test { - def main(args: Array[String]): Unit = { +object Test: + def main(args: Array[String]): Unit = + val r0 = R0() + r0 match + case R0() => + val r1 = R1(42) - r1 match { + r1 match case R1(i) => assert(i == 42) - } - } -} + + val R1(i) = r1 + assert(i == 42) From 7b44500708547672762bb9fc49c3d94e26cc324b Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Fri, 9 Feb 2024 18:06:17 +0000 Subject: [PATCH 05/13] Add generic record run test. --- tests/run-java16+/java-records/R2.java | 1 + tests/run-java16+/java-records/Test.scala | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 tests/run-java16+/java-records/R2.java 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/Test.scala b/tests/run-java16+/java-records/Test.scala index df92c9a8cb31..780832621732 100644 --- a/tests/run-java16+/java-records/Test.scala +++ b/tests/run-java16+/java-records/Test.scala @@ -12,3 +12,10 @@ object Test: 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) From 0d277a1c738c478dedc9fd9b8cabbd412a4cf14e Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Fri, 9 Feb 2024 21:27:59 +0000 Subject: [PATCH 06/13] First-class Java record pattern matching. This adds Java records as a first-class citizen within Scala's pattern matching. Broadly, we synthesize an `inline unapply` on the companion module which we later use to correlate with the bound variables within the pattern matcher. --- .../src/dotty/tools/dotc/ast/Desugar.scala | 7 ++---- .../dotty/tools/dotc/core/Definitions.scala | 1 + .../src/dotty/tools/dotc/core/SymUtils.scala | 4 ++++ .../tools/dotc/transform/PatternMatcher.scala | 24 +++++++++++++++++-- .../dotc/transform/SyntheticMembers.scala | 1 + .../dotty/tools/dotc/typer/Applications.scala | 20 ++++++++++++++++ .../src/dotty/tools/dotc/typer/Namer.scala | 5 +--- tests/run-java16+/java-records/R3.java | 0 8 files changed, 51 insertions(+), 11 deletions(-) create mode 100644 tests/run-java16+/java-records/R3.java diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 8d587cfe1888..eba086e5ad46 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -869,13 +869,10 @@ object desugar { val unapplyRHS = if (arity == 0) Literal(Constant(true)) - else tupleApply( - vParams.map( - param => Select(Ident(unapplyParam.name), param.name) - ) - ) + else Ident(unapplyParam.name) val unapplyResTp = if (arity == 0) Literal(Constant(true)) else TypeTree() + val unapplyMeth = DefDef( nme.unapply, joinParams(derivedTparams, (unapplyParam :: Nil) :: Nil), diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 3cde29ee3d79..6195a79ba0e2 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1636,6 +1636,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/SymUtils.scala b/compiler/src/dotty/tools/dotc/core/SymUtils.scala index 65634241b790..ef64119bcd20 100644 --- a/compiler/src/dotty/tools/dotc/core/SymUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/SymUtils.scala @@ -249,6 +249,10 @@ class SymUtils: def caseAccessors(using Context): List[Symbol] = self.info.decls.filter(_.is(CaseAccessor)) + // TODO: Check if `Synthetic` is stamped properly + def javaRecordComponents(using Context): List[Symbol] = + self.info.decls.filter(sym => sym.is(Synthetic) && sym.is(Method) && !sym.isConstructor) + def getter(using Context): Symbol = if (self.isGetter) self else accessorNamed(self.asTerm.name.getterName) diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index bed29a122399..a4dd0494bc2c 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -343,8 +343,28 @@ object PatternMatcher { .select(defn.RuntimeTuples_apply) .appliedTo(receiver, Literal(Constant(i))) - if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length) - def tupleSel(sym: Symbol) = ref(scrutinee).select(sym) + def resultTypeSym = unapp.symbol.info.resultType.typeSymbol + + def isSyntheticJavaRecordUnapply(sym: Symbol) = + // Since the `unapply` symbol is marked as inline, the `Typer` wraps the body of the `unapply` in a separate + // anonymous class. The result type alone is not enough to distinguish that we're calling the synthesized unapply — + // we could have defined a separate `unapply` method returning a Java record somewhere, hence we resort to using + // the `coord`. + sym.is(Synthetic) && sym.isAnonymousClass && { + val resultSym = resultTypeSym + // TODO: Can a user define a separate unapply function in Java? + val unapplyFn = resultSym.linkedClass.info.decl(nme.unapply) + // TODO: This is nasty, can we add an attachment on the anonymous function for a prior link? + defn.isJavaRecordClass(resultSym) && unapplyFn.symbol.coord == sym.coord + } + + def tupleSel(sym: Symbol) = ref(scrutinee).select(sym) + def recordSel(sym: Symbol) = tupleSel(sym).appliedToTermArgs(Nil) + + if (isSyntheticJavaRecordUnapply(unapp.symbol.owner)) + val components = resultTypeSym.javaRecordComponents.map(recordSel) + matchArgsPlan(components, args, onSuccess) + else if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length) 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) 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/tests/run-java16+/java-records/R3.java b/tests/run-java16+/java-records/R3.java new file mode 100644 index 000000000000..e69de29bb2d1 From 143d5b3f9de70f7c99a288227e6bc1cdbb8e6b2d Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Sat, 10 Feb 2024 16:48:48 +0000 Subject: [PATCH 07/13] Add initial experiments on mirror support. --- .../dotty/tools/dotc/core/Definitions.scala | 3 ++ .../src/dotty/tools/dotc/core/SymUtils.scala | 13 +++++-- .../tools/dotc/transform/PatternMatcher.scala | 18 +++------- .../dotty/tools/dotc/typer/Synthesizer.scala | 8 +++++ .../src/scala/runtime/JavaRecordMirror.scala | 34 +++++++++++++++++++ .../java-records-mirror/FromScala.scala | 5 +++ tests/pos-java16+/java-records-mirror/R2.java | 1 + 7 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 library/src/scala/runtime/JavaRecordMirror.scala create mode 100644 tests/pos-java16+/java-records-mirror/FromScala.scala create mode 100644 tests/pos-java16+/java-records-mirror/R2.java diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 6195a79ba0e2..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") diff --git a/compiler/src/dotty/tools/dotc/core/SymUtils.scala b/compiler/src/dotty/tools/dotc/core/SymUtils.scala index ef64119bcd20..db57e689392c 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,12 @@ 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(sym => sym.is(Synthetic) && sym.is(Method) && !sym.isConstructor) diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index a4dd0494bc2c..0d0fd3b6e900 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -345,23 +345,13 @@ object PatternMatcher { def resultTypeSym = unapp.symbol.info.resultType.typeSymbol - def isSyntheticJavaRecordUnapply(sym: Symbol) = - // Since the `unapply` symbol is marked as inline, the `Typer` wraps the body of the `unapply` in a separate - // anonymous class. The result type alone is not enough to distinguish that we're calling the synthesized unapply — - // we could have defined a separate `unapply` method returning a Java record somewhere, hence we resort to using - // the `coord`. - sym.is(Synthetic) && sym.isAnonymousClass && { - val resultSym = resultTypeSym - // TODO: Can a user define a separate unapply function in Java? - val unapplyFn = resultSym.linkedClass.info.decl(nme.unapply) - // TODO: This is nasty, can we add an attachment on the anonymous function for a prior link? - defn.isJavaRecordClass(resultSym) && unapplyFn.symbol.coord == sym.coord - } - + // 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) - if (isSyntheticJavaRecordUnapply(unapp.symbol.owner)) + // TODO: Move this to the correct location + if (isJavaRecordUnapply(unapp.symbol.owner)) val components = resultTypeSym.javaRecordComponents.map(recordSel) matchArgsPlan(components, args, onSuccess) else if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length) diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index c94724faf4d4..285cecab2033 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -407,6 +407,12 @@ 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 elemLabels = accessors.map(acc => ConstantType(Constant(acc.name.toString))) @@ -427,6 +433,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 +465,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/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/pos-java16+/java-records-mirror/FromScala.scala b/tests/pos-java16+/java-records-mirror/FromScala.scala new file mode 100644 index 000000000000..facf93a12c4e --- /dev/null +++ b/tests/pos-java16+/java-records-mirror/FromScala.scala @@ -0,0 +1,5 @@ +import scala.deriving.Mirror + +object C: + def useR2: Unit = + summon[Mirror.Of[R2]] 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) {} From d65305925c1a001b4d13014dfe31264504b26c0c Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Sat, 10 Feb 2024 18:20:39 +0000 Subject: [PATCH 08/13] Add back previous change. --- .../dotty/tools/dotc/transform/PatternMatcher.scala | 10 +++++----- .../src/dotty/tools/dotc/typer/Synthesizer.scala | 7 +++++-- tests/neg/mirror-synthesis-errors.check | 12 ++++++------ .../pos-java16+/java-records-mirror/FromScala.scala | 3 ++- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index 0d0fd3b6e900..8dd3424e450f 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -351,10 +351,7 @@ object PatternMatcher { def recordSel(sym: Symbol) = tupleSel(sym).appliedToTermArgs(Nil) // TODO: Move this to the correct location - if (isJavaRecordUnapply(unapp.symbol.owner)) - val components = resultTypeSym.javaRecordComponents.map(recordSel) - matchArgsPlan(components, args, onSuccess) - else if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length) + if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length) 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) @@ -379,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/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index 285cecab2033..1683a19b1d78 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -414,9 +414,12 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): .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 => 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-mirror/FromScala.scala b/tests/pos-java16+/java-records-mirror/FromScala.scala index facf93a12c4e..ae4f7aef2cbc 100644 --- a/tests/pos-java16+/java-records-mirror/FromScala.scala +++ b/tests/pos-java16+/java-records-mirror/FromScala.scala @@ -2,4 +2,5 @@ import scala.deriving.Mirror object C: def useR2: Unit = - summon[Mirror.Of[R2]] + val mirror = summon[Mirror.ProductOf[R2]] + mirror.fromTuple((3, "asd")) From b7496721eb603795e617e06da8186ba1d9edf306 Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Sat, 10 Feb 2024 18:33:45 +0000 Subject: [PATCH 09/13] Change return type --- tests/pos-java16+/java-records-mirror/FromScala.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pos-java16+/java-records-mirror/FromScala.scala b/tests/pos-java16+/java-records-mirror/FromScala.scala index ae4f7aef2cbc..81d3d502b536 100644 --- a/tests/pos-java16+/java-records-mirror/FromScala.scala +++ b/tests/pos-java16+/java-records-mirror/FromScala.scala @@ -1,6 +1,6 @@ import scala.deriving.Mirror object C: - def useR2: Unit = + def constructR2: R2 = val mirror = summon[Mirror.ProductOf[R2]] mirror.fromTuple((3, "asd")) From df184afe4fefff92df35e11bd35eaf7c734549d2 Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Wed, 14 Feb 2024 13:09:08 +0000 Subject: [PATCH 10/13] Tentative classfile changes --- .../src/dotty/tools/dotc/core/NamerOps.scala | 36 ++++++++++++++++--- .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../dotty/tools/dotc/core/SymbolLoaders.scala | 1 + .../dotc/core/classfile/ClassfileParser.scala | 28 +++++++++++++-- .../dotty/tools/dotc/CompilationTests.scala | 6 +++- .../dotty/tools/vulpix/ParallelTesting.scala | 22 +++++++++++- .../test/dotty/tools/vulpix/TestFlags.scala | 12 ++++++- .../FromScala.scala | 6 ++++ .../java-records-java-then-scala/R0.java | 1 + .../java-records-java-then-scala/R1.java | 1 + 10 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 tests/pos-java16+/java-records-java-then-scala/FromScala.scala create mode 100644 tests/pos-java16+/java-records-java-then-scala/R0.java create mode 100644 tests/pos-java16+/java-records-java-then-scala/R1.java diff --git a/compiler/src/dotty/tools/dotc/core/NamerOps.scala b/compiler/src/dotty/tools/dotc/core/NamerOps.scala index 75a135826785..1917248afe5a 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 + /** 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,23 @@ object NamerOps: scope end addConstructorApplies + def addIdentUnApply(scope: MutableScope, cls: ClassSymbol, modCls: ClassSymbol)(using 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), + 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? + defn.Predef_identity.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 +172,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 +195,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/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..b0fba7e8ed2a 100644 --- a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala +++ b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala @@ -895,13 +895,15 @@ class ClassfileParser( var exceptions: List[NameOrString] = Nil var annotations: List[Annotation] = Nil var namedParams: Map[Int, TermName] = Map.empty + var recordComponents: List[(NameOrString, NameOrString)] = Nil + 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 } @@ -995,11 +997,33 @@ 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 + res.recordComponents = parseRecord(attrLen) 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(len: Int): List[(NameOrString, NameOrString)] = { + val componentsCount = in.nextChar.toInt + val components = for (_ <- 0 to componentsCount) yield + val name = pool.getName(in.nextChar.toInt) + val descriptor = pool.getClassName(in.nextChar.toInt) + // TODO: Parse the attribute information per component + skipAttributes() + (name, descriptor) + components.toList + } + /** * Parse the "Exceptions" attribute which denotes the exceptions * thrown by a method. diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 3eae946b18b2..8aff484704db 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -53,7 +53,11 @@ class CompilationTests { ) if `isJava16+` then - tests ::= compileFilesInDir("tests/pos-java16+", defaultOptions.and("-Ysafe-init")) + // 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() } 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/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) {} From 6da240bbd8037f9169a7f5caa76a97cd2b8ac0e5 Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Wed, 14 Feb 2024 14:00:07 +0000 Subject: [PATCH 11/13] Hack around with a flag for now. --- .../dotc/core/classfile/ClassfileParser.scala | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala index b0fba7e8ed2a..f6fea7c126e4 100644 --- a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala +++ b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala @@ -895,7 +895,6 @@ class ClassfileParser( var exceptions: List[NameOrString] = Nil var annotations: List[Annotation] = Nil var namedParams: Map[Int, TermName] = Map.empty - var recordComponents: List[(NameOrString, NameOrString)] = Nil def complete(tp: Type, isVarargs: Boolean = false)(using Context): Type = { val updatedType = @@ -913,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) @@ -1000,7 +999,7 @@ class ClassfileParser( // https://docs.oracle.com/javase/specs/jvms/se15/preview/specs/records-jvms.html case tpnme.RecordATTR => // Each member is /not/ sythetic on purpose - res.recordComponents = parseRecord(attrLen) + parseRecord() case _ => } in.bp = end @@ -1013,15 +1012,21 @@ class ClassfileParser( * each component within the `Record` in the order of the canonical * constructor. */ - def parseRecord(len: Int): List[(NameOrString, NameOrString)] = { + def parseRecord(): Unit = { val componentsCount = in.nextChar.toInt - val components = for (_ <- 0 to componentsCount) yield - val name = pool.getName(in.nextChar.toInt) - val descriptor = pool.getClassName(in.nextChar.toInt) - // TODO: Parse the attribute information per component + 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) + + // TODO: Do we want to respect the JVM and /not/ stamp this as `Synthetic`? + instanceScope.lookup(name.name).setFlag(Flags.Synthetic) + + // TODO: Double /where/ we want these attributes to sit. skipAttributes() - (name, descriptor) - components.toList + (name, name) } /** From 3aa99e72b3aac305592d7eaf58c6889672ce68d0 Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Wed, 14 Feb 2024 15:26:48 +0000 Subject: [PATCH 12/13] Switch over to using `ParamAccessor` as discriminant. --- compiler/src/dotty/tools/dotc/core/SymUtils.scala | 2 +- .../src/dotty/tools/dotc/core/classfile/ClassfileParser.scala | 3 +-- compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala | 3 ++- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/SymUtils.scala b/compiler/src/dotty/tools/dotc/core/SymUtils.scala index db57e689392c..462550271aa8 100644 --- a/compiler/src/dotty/tools/dotc/core/SymUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/SymUtils.scala @@ -258,7 +258,7 @@ class SymUtils: // TODO: Check if `Synthetic` is stamped properly def javaRecordComponents(using Context): List[Symbol] = - self.info.decls.filter(sym => sym.is(Synthetic) && sym.is(Method) && !sym.isConstructor) + 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/classfile/ClassfileParser.scala b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala index f6fea7c126e4..303a00f7e2ba 100644 --- a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala +++ b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala @@ -1021,8 +1021,7 @@ class ClassfileParser( val name = pool.getName(nameIndex) val descriptor = pool.getName(descriptorIndex) - // TODO: Do we want to respect the JVM and /not/ stamp this as `Synthetic`? - instanceScope.lookup(name.name).setFlag(Flags.Synthetic) + instanceScope.lookup(name.name).setFlag(Flags.ParamAccessor) // TODO: Double /where/ we want these attributes to sit. skipAttributes() 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 From 353d9d83436517affe9c77e627b949e34512957d Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Fri, 16 Feb 2024 20:47:48 +0000 Subject: [PATCH 13/13] Adding step to reduce `unapply`. --- compiler/src/dotty/tools/dotc/core/NamerOps.scala | 9 +++++---- .../src/dotty/tools/dotc/transform/Inlining.scala | 12 +++++++++++- .../src/dotty/tools/dotc/transform/PostTyper.scala | 4 +++- .../test/dotty/tools/dotc/CompilationTests.scala | 1 + 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/NamerOps.scala b/compiler/src/dotty/tools/dotc/core/NamerOps.scala index 1917248afe5a..cebe6ed44b14 100644 --- a/compiler/src/dotty/tools/dotc/core/NamerOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NamerOps.scala @@ -83,7 +83,7 @@ object NamerOps: 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 + 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 @@ -126,18 +126,19 @@ object NamerOps: scope end addConstructorApplies - def addIdentUnApply(scope: MutableScope, cls: ClassSymbol, modCls: ClassSymbol)(using Context): scope.type = + 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), + 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? - defn.Predef_identity.coord + constr.coord ) + val decl = cls.info.decls.find(_.isConstructor) scope.enter(proxy(decl)) scope 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/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/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 8aff484704db..bee2eb6315f4 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -171,6 +171,7 @@ class CompilationTests { 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() }