From 40cf267c23d7e5c38bff979a4af6ce996e12b134 Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Fri, 24 Nov 2023 14:54:58 +0100 Subject: [PATCH 1/4] fix sealedStrictDescendants for Java enum - Rename JavaEnumTrait flags to JavaEnum (the actual flags set) - test java enum in SealedDescendantsTest --- .../tools/backend/jvm/BCodeHelpers.scala | 2 +- .../tools/backend/jvm/BTypesFromSymbols.scala | 2 +- .../src/dotty/tools/dotc/core/Flags.scala | 2 +- .../tools/dotc/core/SymDenotations.scala | 2 +- .../tools/dotc/parsing/JavaParsers.scala | 4 +-- .../tools/dotc/transform/patmat/Space.scala | 4 +-- .../dotc/core/SealedDescendantsTest.scala | 33 ++++++++++++++++--- .../pc/completions/MatchCaseCompletions.scala | 10 ++---- .../backend/jvm/BCodeHelpers.scala | 2 +- .../backend/jvm/BTypesFromSymbols.scala | 2 +- 10 files changed, 41 insertions(+), 22 deletions(-) diff --git a/compiler/src/dotty/tools/backend/jvm/BCodeHelpers.scala b/compiler/src/dotty/tools/backend/jvm/BCodeHelpers.scala index 3779f59d33b0..6331913049c2 100644 --- a/compiler/src/dotty/tools/backend/jvm/BCodeHelpers.scala +++ b/compiler/src/dotty/tools/backend/jvm/BCodeHelpers.scala @@ -292,7 +292,7 @@ trait BCodeHelpers extends BCodeIdiomatic { } case Ident(nme.WILDCARD) => // An underscore argument indicates that we want to use the default value for this parameter, so do not emit anything - case t: tpd.RefTree if t.symbol.owner.linkedClass.isAllOf(JavaEnumTrait) => + case t: tpd.RefTree if t.symbol.owner.linkedClass.isAllOf(JavaEnum) => val edesc = innerClasesStore.typeDescriptor(t.tpe) // the class descriptor of the enumeration class. val evalue = t.symbol.javaSimpleName // value the actual enumeration value. av.visitEnum(name, edesc, evalue) diff --git a/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala b/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala index 55619f31ec32..b8d7ee04c870 100644 --- a/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala +++ b/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala @@ -304,7 +304,7 @@ class BTypesFromSymbols[I <: DottyBackendInterface](val int: I, val frontendAcce .addFlagIf(sym.is(Bridge), ACC_BRIDGE | ACC_SYNTHETIC) .addFlagIf(sym.is(Artifact), ACC_SYNTHETIC) .addFlagIf(sym.isClass && !sym.isInterface, ACC_SUPER) - .addFlagIf(sym.isAllOf(JavaEnumTrait), ACC_ENUM) + .addFlagIf(sym.isAllOf(JavaEnum), ACC_ENUM) .addFlagIf(sym.is(JavaVarargs), ACC_VARARGS) .addFlagIf(sym.is(Synchronized), ACC_SYNCHRONIZED) .addFlagIf(sym.isDeprecated, ACC_DEPRECATED) diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index 6ae9541a327f..8c1b715e3e30 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -576,7 +576,7 @@ object Flags { val InlineMethod: FlagSet = Inline | Method val InlineParam: FlagSet = Inline | Param val InlineByNameProxy: FlagSet = InlineProxy | Method - val JavaEnumTrait: FlagSet = JavaDefined | Enum // A Java enum trait + val JavaEnum: FlagSet = JavaDefined | Enum // A Java enum trait val JavaEnumValue: FlagSet = JavaDefined | EnumValue // A Java enum value val StaticProtected: FlagSet = JavaDefined | JavaStatic | Protected // Java symbol which is `protected` and `static` val JavaModule: FlagSet = JavaDefined | Module // A Java companion object diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 5fa9afbcd171..83362897e5e3 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -1695,7 +1695,7 @@ object SymDenotations { c.ensureCompleted() end completeChildrenIn - if is(Sealed) || isAllOf(JavaEnumTrait) then + if is(Sealed) || isAllOf(JavaEnum) && isClass then if !is(ChildrenQueried) then // Make sure all visible children are completed, so that // they show up in Child annotations. A possible child is visible if it diff --git a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala index 8e075acdf5e3..1c1c47ad68ce 100644 --- a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala @@ -992,7 +992,7 @@ object JavaParsers { Select(New(javaLangDot(tpnme.Enum)), nme.CONSTRUCTOR), List(enumType)), Nil) val enumclazz = atSpan(start, nameOffset) { TypeDef(name, - makeTemplate(superclazz :: interfaces, body, List(), true)).withMods(mods | Flags.JavaEnumTrait) + makeTemplate(superclazz :: interfaces, body, List(), true)).withMods(mods | Flags.JavaEnum) } addCompanionObject(consts ::: statics ::: predefs, enumclazz) } @@ -1011,7 +1011,7 @@ object JavaParsers { skipAhead() accept(RBRACE) } - ValDef(name.toTermName, enumType, unimplementedExpr).withMods(Modifiers(Flags.JavaEnumTrait | Flags.StableRealizable | Flags.JavaDefined | Flags.JavaStatic)) + ValDef(name.toTermName, enumType, unimplementedExpr).withMods(Modifiers(Flags.JavaEnumValue | Flags.JavaStatic)) } } diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index f3a883df9e4f..beb4119775d0 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -626,7 +626,7 @@ object SpaceEngine { case tp if tp.isRef(defn.UnitClass) => ConstantType(Constant(())) :: Nil case tp @ NamedType(Parts(parts), _) => parts.map(tp.derivedSelect) case _: SingletonType => ListOfNoType - case tp if tp.classSymbol.isAllOf(JavaEnumTrait) => tp.classSymbol.children.map(_.termRef) + case tp if tp.classSymbol.isAllOf(JavaEnum) => tp.classSymbol.children.map(_.termRef) // the class of a java enum value is the enum class, so this must follow SingletonType to not loop infinitely case tp @ AppliedType(Parts(parts), targs) if tp.classSymbol.children.isEmpty => @@ -843,7 +843,7 @@ object SpaceEngine { isCheckable(and.tp1) || isCheckable(and.tp2) }) || tpw.isRef(defn.BooleanClass) || - classSym.isAllOf(JavaEnumTrait) || + classSym.isAllOf(JavaEnum) || classSym.is(Case) && { if seen.add(tpw) then productSelectorTypes(tpw, sel.srcPos).exists(isCheckable(_)) else true // recursive case class: return true and other members can still fail the check diff --git a/compiler/test/dotty/tools/dotc/core/SealedDescendantsTest.scala b/compiler/test/dotty/tools/dotc/core/SealedDescendantsTest.scala index 0ae9069c03d1..4726596c0428 100644 --- a/compiler/test/dotty/tools/dotc/core/SealedDescendantsTest.scala +++ b/compiler/test/dotty/tools/dotc/core/SealedDescendantsTest.scala @@ -47,6 +47,19 @@ class SealedDescendantsTest extends DottyTest { ) end enumOpt + @Test + def javaEnum: Unit = + expectedDescendents("java.util.concurrent.TimeUnit", + "TimeUnit" :: + "NANOSECONDS.type" :: + "MICROSECONDS.type" :: + "MILLISECONDS.type" :: + "SECONDS.type" :: + "MINUTES.type" :: + "HOURS.type" :: + "DAYS.type" :: Nil + ) + @Test def hierarchicalSharedChildren: Unit = // Q is a child of both Z and A and should appear once @@ -91,10 +104,22 @@ class SealedDescendantsTest extends DottyTest { ) end hierarchicalSharedChildrenB - def expectedDescendents(source: String, root: String, expected: List[String]) = - exploreRoot(source, root) { rootCls => - val descendents = rootCls.sealedDescendants.map(sym => s"${sym.name}${if (sym.isTerm) ".type" else ""}") - assertEquals(expected.toString, descendents.toString) + def assertMatchingDescenants(rootCls: Symbol, expected: List[String])(using Context): Unit = + val descendents = rootCls.sealedDescendants.map(sym => s"${sym.name}${if (sym.isTerm) ".type" else ""}") + assertEquals(expected.toString, descendents.toString) + + def expectedDescendents(root: String, expected: List[String]): Unit = + exploreRootNoSource(root)(assertMatchingDescenants(_, expected)) + + def expectedDescendents(source: String, root: String, expected: List[String]): Unit = + exploreRoot(source, root)(assertMatchingDescenants(_, expected)) + + def exploreRootNoSource(root: String)(op: Context ?=> ClassSymbol => Unit) = + val source1 = s"""package testsealeddescendants + |object Foo { def foo: $root = ??? }""".stripMargin + checkCompile("typer", source1) { (_, context) => + given Context = context + op(requiredClass(root)) } def exploreRoot(source: String, root: String)(op: Context ?=> ClassSymbol => Unit) = diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/MatchCaseCompletions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/MatchCaseCompletions.scala index fe9a73655835..a3d5d8814c48 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/MatchCaseCompletions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/MatchCaseCompletions.scala @@ -349,11 +349,7 @@ object CaseKeywordCompletion: symTpe <:< tpe val parents = getParentTypes(tpe, List.empty) - parents.toList.map { parent => - // There is an issue in Dotty, `sealedStrictDescendants` ends in an exception for java enums. https://github.com/lampepfl/dotty/issues/15908 - if parent.isAllOf(JavaEnumTrait) then parent.children - else sealedStrictDescendants(parent) - } match + parents.toList.map(sealedStrictDescendants) match case Nil => Nil case subcls :: Nil => subcls case subcls => @@ -409,9 +405,7 @@ class CompletionValueGenerator( Context ): Option[String] = val isModuleLike = - sym.is(Flags.Module) || sym.isOneOf(JavaEnumTrait) || sym.isOneOf( - JavaEnumValue - ) || sym.isAllOf(EnumCase) + sym.is(Flags.Module) || sym.isOneOf(JavaEnum) || sym.isOneOf(JavaEnumValue) || sym.isAllOf(EnumCase) if isModuleLike && hasBind then None else val pattern = diff --git a/tests/pos-with-compiler-cc/backend/jvm/BCodeHelpers.scala b/tests/pos-with-compiler-cc/backend/jvm/BCodeHelpers.scala index 3ab75bda787e..2454bca9d653 100644 --- a/tests/pos-with-compiler-cc/backend/jvm/BCodeHelpers.scala +++ b/tests/pos-with-compiler-cc/backend/jvm/BCodeHelpers.scala @@ -374,7 +374,7 @@ trait BCodeHelpers extends BCodeIdiomatic with BytecodeWriters { } case Ident(nme.WILDCARD) => // An underscore argument indicates that we want to use the default value for this parameter, so do not emit anything - case t: tpd.RefTree if t.symbol.owner.linkedClass.isAllOf(JavaEnumTrait) => + case t: tpd.RefTree if t.symbol.owner.linkedClass.isAllOf(JavaEnum) => val edesc = innerClasesStore.typeDescriptor(t.tpe) // the class descriptor of the enumeration class. val evalue = t.symbol.javaSimpleName // value the actual enumeration value. av.visitEnum(name, edesc, evalue) diff --git a/tests/pos-with-compiler-cc/backend/jvm/BTypesFromSymbols.scala b/tests/pos-with-compiler-cc/backend/jvm/BTypesFromSymbols.scala index 54dafe6f0032..d78008d65cc6 100644 --- a/tests/pos-with-compiler-cc/backend/jvm/BTypesFromSymbols.scala +++ b/tests/pos-with-compiler-cc/backend/jvm/BTypesFromSymbols.scala @@ -330,7 +330,7 @@ class BTypesFromSymbols[I <: DottyBackendInterface](val int: I) extends BTypes { .addFlagIf(sym.is(Bridge), ACC_BRIDGE | ACC_SYNTHETIC) .addFlagIf(sym.is(Artifact), ACC_SYNTHETIC) .addFlagIf(sym.isClass && !sym.isInterface, ACC_SUPER) - .addFlagIf(sym.isAllOf(JavaEnumTrait), ACC_ENUM) + .addFlagIf(sym.isAllOf(JavaEnum), ACC_ENUM) .addFlagIf(sym.is(JavaVarargs), ACC_VARARGS) .addFlagIf(sym.is(Synchronized), ACC_SYNCHRONIZED) .addFlagIf(sym.isDeprecated, ACC_DEPRECATED) From 7fa6565fa91874eb0f0c27d846a72f4f7d7fc6b9 Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Mon, 27 Nov 2023 17:42:36 +0100 Subject: [PATCH 2/4] add CompilationUnitInfo to the CompilationUnit --- .../src/dotty/tools/dotc/CompilationUnit.scala | 16 +++++++++------- .../fromtasty/AlreadyLoadedCompilationUnit.scala | 2 +- .../dotc/fromtasty/TASTYCompilationUnit.scala | 2 +- compiler/src/dotty/tools/dotc/typer/Namer.scala | 2 +- compiler/src/dotty/tools/repl/ReplCompiler.scala | 3 ++- .../quoted/staging/ExprCompilationUnit.scala | 2 +- .../src/scala/quoted/staging/QuoteCompiler.scala | 3 ++- 7 files changed, 17 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/CompilationUnit.scala b/compiler/src/dotty/tools/dotc/CompilationUnit.scala index 62f62a3c6dc6..39e94facb26a 100644 --- a/compiler/src/dotty/tools/dotc/CompilationUnit.scala +++ b/compiler/src/dotty/tools/dotc/CompilationUnit.scala @@ -19,7 +19,7 @@ import scala.annotation.internal.sharable import scala.util.control.NoStackTrace import transform.MacroAnnotations -class CompilationUnit protected (val source: SourceFile) { +class CompilationUnit protected (val source: SourceFile, val info: CompilationUnitInfo | Null) { override def toString: String = source.toString @@ -106,7 +106,7 @@ class CompilationUnit protected (val source: SourceFile) { myAssignmentSpans.nn } -@sharable object NoCompilationUnit extends CompilationUnit(NoSource) { +@sharable object NoCompilationUnit extends CompilationUnit(NoSource, info = null) { override def isJava: Boolean = false @@ -122,13 +122,14 @@ object CompilationUnit { /** Make a compilation unit for top class `clsd` with the contents of the `unpickled` tree */ def apply(clsd: ClassDenotation, unpickled: Tree, forceTrees: Boolean)(using Context): CompilationUnit = - val file = clsd.symbol.associatedFile.nn - apply(SourceFile(file, Array.empty[Char]), unpickled, forceTrees) + val compilationUnitInfo = clsd.symbol.compilationUnitInfo.nn + val file = compilationUnitInfo.associatedFile + apply(SourceFile(file, Array.empty[Char]), unpickled, forceTrees, compilationUnitInfo) /** Make a compilation unit, given picked bytes and unpickled tree */ - def apply(source: SourceFile, unpickled: Tree, forceTrees: Boolean)(using Context): CompilationUnit = { + def apply(source: SourceFile, unpickled: Tree, forceTrees: Boolean, info: CompilationUnitInfo)(using Context): CompilationUnit = { assert(!unpickled.isEmpty, unpickled) - val unit1 = new CompilationUnit(source) + val unit1 = new CompilationUnit(source, info) unit1.tpdTree = unpickled if (forceTrees) { val force = new Force @@ -156,7 +157,8 @@ object CompilationUnit { NoSource } else source - new CompilationUnit(src) + val info = if src.exists then CompilationUnitInfo(src.file) else null + new CompilationUnit(src, info) } /** Force the tree to be loaded */ diff --git a/compiler/src/dotty/tools/dotc/fromtasty/AlreadyLoadedCompilationUnit.scala b/compiler/src/dotty/tools/dotc/fromtasty/AlreadyLoadedCompilationUnit.scala index 74c680bda1b7..8700cb730e91 100644 --- a/compiler/src/dotty/tools/dotc/fromtasty/AlreadyLoadedCompilationUnit.scala +++ b/compiler/src/dotty/tools/dotc/fromtasty/AlreadyLoadedCompilationUnit.scala @@ -7,4 +7,4 @@ import dotty.tools.dotc.util.NoSource * encountered, and attempted to inspect, something that has already been loaded, for example a Scala primitive or a * library class like Option. */ -class AlreadyLoadedCompilationUnit(val className: String) extends CompilationUnit(NoSource) +class AlreadyLoadedCompilationUnit(val className: String) extends CompilationUnit(NoSource, null) diff --git a/compiler/src/dotty/tools/dotc/fromtasty/TASTYCompilationUnit.scala b/compiler/src/dotty/tools/dotc/fromtasty/TASTYCompilationUnit.scala index 77021efa3050..1d4daff510e7 100644 --- a/compiler/src/dotty/tools/dotc/fromtasty/TASTYCompilationUnit.scala +++ b/compiler/src/dotty/tools/dotc/fromtasty/TASTYCompilationUnit.scala @@ -3,6 +3,6 @@ package dotty.tools.dotc.fromtasty import dotty.tools.dotc.CompilationUnit import dotty.tools.dotc.util.NoSource -class TASTYCompilationUnit(val className: String) extends CompilationUnit(NoSource) { +class TASTYCompilationUnit(val className: String) extends CompilationUnit(NoSource, null) { override def toString: String = s"class file $className" } diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 5697fcdfc98f..f8ced1c6599a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -248,7 +248,7 @@ class Namer { typer: Typer => val cls = createOrRefine[ClassSymbol](tree, name, flags, ctx.owner, cls => adjustIfModule(new ClassCompleter(cls, tree)(ctx), tree), - newClassSymbol(ctx.owner, name, _, _, _, tree.nameSpan, CompilationUnitInfo(ctx.source.file))) + newClassSymbol(ctx.owner, name, _, _, _, tree.nameSpan, ctx.compilationUnit.info)) cls.completer.asInstanceOf[ClassCompleter].init() cls case tree: MemberDef => diff --git a/compiler/src/dotty/tools/repl/ReplCompiler.scala b/compiler/src/dotty/tools/repl/ReplCompiler.scala index af3fb32c3e86..d69173cb6d88 100644 --- a/compiler/src/dotty/tools/repl/ReplCompiler.scala +++ b/compiler/src/dotty/tools/repl/ReplCompiler.scala @@ -4,6 +4,7 @@ import dotty.tools.dotc.ast.Trees.* import dotty.tools.dotc.ast.{tpd, untpd} import dotty.tools.dotc.ast.tpd.TreeOps import dotty.tools.dotc.core.Contexts.* +import dotty.tools.dotc.core.CompilationUnitInfo import dotty.tools.dotc.core.Decorators.* import dotty.tools.dotc.core.Flags.* import dotty.tools.dotc.core.Names.* @@ -233,7 +234,7 @@ object ReplCompiler: val objectNames = mutable.Map.empty[Int, TermName] end ReplCompiler -class ReplCompilationUnit(source: SourceFile) extends CompilationUnit(source): +class ReplCompilationUnit(source: SourceFile) extends CompilationUnit(source, CompilationUnitInfo(source.file)): override def isSuspendable: Boolean = false /** A placeholder phase that receives parse trees.. diff --git a/staging/src/scala/quoted/staging/ExprCompilationUnit.scala b/staging/src/scala/quoted/staging/ExprCompilationUnit.scala index ea1456527c8b..2f91f4bc4ef1 100644 --- a/staging/src/scala/quoted/staging/ExprCompilationUnit.scala +++ b/staging/src/scala/quoted/staging/ExprCompilationUnit.scala @@ -5,4 +5,4 @@ import dotty.tools.dotc.CompilationUnit import dotty.tools.dotc.util.NoSource /** Compilation unit containing the contents of a quoted expression */ -private class ExprCompilationUnit(val exprBuilder: Quotes => Expr[?]) extends CompilationUnit(NoSource) +private class ExprCompilationUnit(val exprBuilder: Quotes => Expr[?]) extends CompilationUnit(NoSource, null) diff --git a/staging/src/scala/quoted/staging/QuoteCompiler.scala b/staging/src/scala/quoted/staging/QuoteCompiler.scala index d10eab321bae..cf24b1de369a 100644 --- a/staging/src/scala/quoted/staging/QuoteCompiler.scala +++ b/staging/src/scala/quoted/staging/QuoteCompiler.scala @@ -95,8 +95,9 @@ private class QuoteCompiler extends Compiler: val classTree = ClassDef(cls, DefDef(cls.primaryConstructor.asTerm), run :: Nil) val tree = PackageDef(ref(defn.RootPackage).asInstanceOf[Ident], classTree :: Nil).withSpan(pos) val source = SourceFile.virtual("", "") + val unitInfo = CompilationUnitInfo(source.file, tastyInfo = None) result = Left(outputClassName.toString) - Some(CompilationUnit(source, tree, forceTrees = true)) + Some(CompilationUnit(source, tree, forceTrees = true, unitInfo)) } /** Get the literal value if this tree only contains a literal tree */ From 3e8e563c1024822f3cf3156c1026e946aae958e2 Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Mon, 6 Nov 2023 17:11:44 +0100 Subject: [PATCH 3/4] Support the -Yjava-tasty flag. - Keep Java compilation units up to Pickler phase if -Yjava-tasty. Skip phases for Java when not needed. - Add JAVAattr and OUTLINEattr TASTy attributes, ELIDED tree tag. ELIDED trees are pickled as rhs of java term definitions. ELIDED trees can only be unpickled if OUTLINEattr is present. For now OUTLINEattr implies JAVAattr. In the future we might expand OUTLINEattr to include outline Scala typing. - write java tasty files to a special jar, set with -Yjava-tasty-output this option is for testing purposes only. --- .../dotty/tools/dotc/CompilationUnit.scala | 7 ++ .../src/dotty/tools/dotc/ast/Desugar.scala | 15 +++- .../tools/dotc/config/ScalaSettings.scala | 7 +- .../dotty/tools/dotc/core/Definitions.scala | 1 + .../src/dotty/tools/dotc/core/Phases.scala | 11 ++- .../tools/dotc/core/tasty/Attributes.scala | 6 ++ .../dotc/core/tasty/DottyUnpickler.scala | 1 + .../tools/dotc/core/tasty/TreePickler.scala | 14 +++- .../tools/dotc/core/tasty/TreeUnpickler.scala | 24 +++++- .../tools/dotc/fromtasty/ReadTasty.scala | 13 ++- .../tools/dotc/parsing/JavaParsers.scala | 2 +- .../dotty/tools/dotc/parsing/Parsers.scala | 6 +- .../tools/dotc/quoted/PickledQuotes.scala | 4 +- .../src/dotty/tools/dotc/sbt/ExtractAPI.scala | 3 + .../tools/dotc/sbt/ExtractDependencies.scala | 3 + .../dotty/tools/dotc/transform/Pickler.scala | 82 +++++++++++++++---- .../dotty/tools/dotc/typer/TyperPhase.scala | 2 +- .../dotty/tools/io/ClassfileWriterOps.scala | 50 +++++++++++ compiler/src/dotty/tools/io/JarArchive.scala | 1 + .../a/src/main/scala/a/A.java | 11 +++ .../a/src/main/scala/a/package.scala | 2 + .../b/src/main/scala/b/B.scala | 9 ++ .../Yjava-tasty-annotation/build.sbt | 13 +++ .../project/DottyInjectedPlugin.scala | 12 +++ .../pipelining/Yjava-tasty-annotation/test | 3 + .../a/src/main/scala/a/A.java | 7 ++ .../a/src/main/scala/a/package.scala | 2 + .../b/src/main/scala/b/B.scala | 17 ++++ .../pipelining/Yjava-tasty-enum/build.sbt | 15 ++++ .../project/DottyInjectedPlugin.scala | 12 +++ sbt-test/pipelining/Yjava-tasty-enum/test | 3 + .../a/src/main/scala/a/A.java | 5 ++ .../a/src/main/scala/a/package.scala | 2 + .../Yjava-tasty-from-tasty/a_from_tasty/.keep | 0 .../b/src/main/scala/b/B.scala | 5 ++ .../Yjava-tasty-from-tasty/build.sbt | 29 +++++++ .../project/DottyInjectedPlugin.scala | 12 +++ .../pipelining/Yjava-tasty-from-tasty/test | 5 ++ .../a/src/main/scala/a/A.java | 14 ++++ .../a/src/main/scala/a/package.scala | 2 + .../b/src/main/scala/b/B.scala | 12 +++ .../pipelining/Yjava-tasty-generic/build.sbt | 13 +++ .../project/DottyInjectedPlugin.scala | 12 +++ sbt-test/pipelining/Yjava-tasty-generic/test | 3 + .../a/src/main/scala/a/A.java | 9 ++ .../a/src/main/scala/a/package.scala | 2 + .../b/src/main/scala/b/B.scala | 17 ++++ .../Yjava-tasty-result-types/build.sbt | 13 +++ .../project/DottyInjectedPlugin.scala | 12 +++ .../pipelining/Yjava-tasty-result-types/test | 3 + tasty/src/dotty/tools/tasty/TastyFormat.scala | 11 ++- 51 files changed, 504 insertions(+), 35 deletions(-) create mode 100644 compiler/src/dotty/tools/io/ClassfileWriterOps.scala create mode 100644 sbt-test/pipelining/Yjava-tasty-annotation/a/src/main/scala/a/A.java create mode 100644 sbt-test/pipelining/Yjava-tasty-annotation/a/src/main/scala/a/package.scala create mode 100644 sbt-test/pipelining/Yjava-tasty-annotation/b/src/main/scala/b/B.scala create mode 100644 sbt-test/pipelining/Yjava-tasty-annotation/build.sbt create mode 100644 sbt-test/pipelining/Yjava-tasty-annotation/project/DottyInjectedPlugin.scala create mode 100644 sbt-test/pipelining/Yjava-tasty-annotation/test create mode 100644 sbt-test/pipelining/Yjava-tasty-enum/a/src/main/scala/a/A.java create mode 100644 sbt-test/pipelining/Yjava-tasty-enum/a/src/main/scala/a/package.scala create mode 100644 sbt-test/pipelining/Yjava-tasty-enum/b/src/main/scala/b/B.scala create mode 100644 sbt-test/pipelining/Yjava-tasty-enum/build.sbt create mode 100644 sbt-test/pipelining/Yjava-tasty-enum/project/DottyInjectedPlugin.scala create mode 100644 sbt-test/pipelining/Yjava-tasty-enum/test create mode 100644 sbt-test/pipelining/Yjava-tasty-from-tasty/a/src/main/scala/a/A.java create mode 100644 sbt-test/pipelining/Yjava-tasty-from-tasty/a/src/main/scala/a/package.scala create mode 100644 sbt-test/pipelining/Yjava-tasty-from-tasty/a_from_tasty/.keep create mode 100644 sbt-test/pipelining/Yjava-tasty-from-tasty/b/src/main/scala/b/B.scala create mode 100644 sbt-test/pipelining/Yjava-tasty-from-tasty/build.sbt create mode 100644 sbt-test/pipelining/Yjava-tasty-from-tasty/project/DottyInjectedPlugin.scala create mode 100644 sbt-test/pipelining/Yjava-tasty-from-tasty/test create mode 100644 sbt-test/pipelining/Yjava-tasty-generic/a/src/main/scala/a/A.java create mode 100644 sbt-test/pipelining/Yjava-tasty-generic/a/src/main/scala/a/package.scala create mode 100644 sbt-test/pipelining/Yjava-tasty-generic/b/src/main/scala/b/B.scala create mode 100644 sbt-test/pipelining/Yjava-tasty-generic/build.sbt create mode 100644 sbt-test/pipelining/Yjava-tasty-generic/project/DottyInjectedPlugin.scala create mode 100644 sbt-test/pipelining/Yjava-tasty-generic/test create mode 100644 sbt-test/pipelining/Yjava-tasty-result-types/a/src/main/scala/a/A.java create mode 100644 sbt-test/pipelining/Yjava-tasty-result-types/a/src/main/scala/a/package.scala create mode 100644 sbt-test/pipelining/Yjava-tasty-result-types/b/src/main/scala/b/B.scala create mode 100644 sbt-test/pipelining/Yjava-tasty-result-types/build.sbt create mode 100644 sbt-test/pipelining/Yjava-tasty-result-types/project/DottyInjectedPlugin.scala create mode 100644 sbt-test/pipelining/Yjava-tasty-result-types/test diff --git a/compiler/src/dotty/tools/dotc/CompilationUnit.scala b/compiler/src/dotty/tools/dotc/CompilationUnit.scala index 39e94facb26a..78773a518b67 100644 --- a/compiler/src/dotty/tools/dotc/CompilationUnit.scala +++ b/compiler/src/dotty/tools/dotc/CompilationUnit.scala @@ -30,6 +30,13 @@ class CompilationUnit protected (val source: SourceFile, val info: CompilationUn /** Is this the compilation unit of a Java file */ def isJava: Boolean = source.file.name.endsWith(".java") + /** Is this the compilation unit of a Java file, or TASTy derived from a Java file */ + def typedAsJava = isJava || { + val infoNN = info + infoNN != null && infoNN.tastyInfo.exists(_.attributes.isJava) + } + + /** The source version for this unit, as determined by a language import */ var sourceVersion: Option[SourceVersion] = None diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index a08d6da650c9..3386dc7d7a6c 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -17,6 +17,7 @@ import reporting.* import annotation.constructorOnly import printing.Formatting.hl import config.Printers +import parsing.Parsers import scala.annotation.internal.sharable import scala.annotation.threadUnsafe @@ -143,8 +144,13 @@ object desugar { /** A value definition copied from `vdef` with a tpt typetree derived from it */ def derivedTermParam(vdef: ValDef)(using Context): ValDef = + derivedTermParam(vdef, vdef.unforcedRhs) + + def derivedTermParam(vdef: ValDef, rhs: LazyTree)(using Context): ValDef = cpy.ValDef(vdef)( - tpt = DerivedFromParamTree().withSpan(vdef.tpt.span).watching(vdef)) + tpt = DerivedFromParamTree().withSpan(vdef.tpt.span).watching(vdef), + rhs = rhs + ) // ----- Desugar methods ------------------------------------------------- @@ -544,8 +550,11 @@ object desugar { constrTparams.zipWithConserve(impliedTparams)((tparam, impliedParam) => derivedTypeParam(tparam).withAnnotations(impliedParam.mods.annotations)) val derivedVparamss = - constrVparamss.nestedMap(vparam => - derivedTermParam(vparam).withAnnotations(Nil)) + constrVparamss.nestedMap: vparam => + val derived = + if ctx.compilationUnit.isJava then derivedTermParam(vparam, Parsers.unimplementedExpr) + else derivedTermParam(vparam) + derived.withAnnotations(Nil) val constr = cpy.DefDef(constr1)(paramss = joinParams(constrTparams, constrVparamss)) diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 68c68e48caaf..a71f28f49410 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -8,7 +8,7 @@ import dotty.tools.dotc.config.Settings.{Setting, SettingGroup} import dotty.tools.dotc.config.SourceVersion import dotty.tools.dotc.core.Contexts.* import dotty.tools.dotc.rewrites.Rewrites -import dotty.tools.io.{AbstractFile, Directory, JDK9Reflectors, PlainDirectory} +import dotty.tools.io.{AbstractFile, Directory, JDK9Reflectors, PlainDirectory, NoAbstractFile} import Setting.ChoiceWithHelp import scala.util.chaining.* @@ -433,4 +433,9 @@ private sealed trait YSettings: val YforceInlineWhileTyping: Setting[Boolean] = BooleanSetting("-Yforce-inline-while-typing", "Make non-transparent inline methods inline when typing. Emulates the old inlining behavior of 3.0.0-M3.") val YdebugMacros: Setting[Boolean] = BooleanSetting("-Ydebug-macros", "Show debug info when quote pattern match fails") + + // Pipeline compilation options + val YjavaTasty: Setting[Boolean] = BooleanSetting("-Yjava-tasty", "Pickler phase should compute pickles for .java defined symbols for use by build tools") + val YjavaTastyOutput: Setting[AbstractFile] = OutputSetting("-Yjava-tasty-output", "directory|jar", "(Internal use only!) destination for generated .tasty files containing Java type signatures.", NoAbstractFile) + val YallowOutlineFromTasty: Setting[Boolean] = BooleanSetting("-Yallow-outline-from-tasty", "Allow outline TASTy to be loaded with the -from-tasty option.") end YSettings diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index c3de3ab69519..3d86b0271e02 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -996,6 +996,7 @@ class Definitions { @tu lazy val AnnotationClass: ClassSymbol = requiredClass("scala.annotation.Annotation") @tu lazy val StaticAnnotationClass: ClassSymbol = requiredClass("scala.annotation.StaticAnnotation") @tu lazy val RefiningAnnotationClass: ClassSymbol = requiredClass("scala.annotation.RefiningAnnotation") + @tu lazy val JavaAnnotationClass: ClassSymbol = requiredClass("java.lang.annotation.Annotation") // Annotation classes @tu lazy val AllowConversionsAnnot: ClassSymbol = requiredClass("scala.annotation.allowConversions") diff --git a/compiler/src/dotty/tools/dotc/core/Phases.scala b/compiler/src/dotty/tools/dotc/core/Phases.scala index e04d829d1e60..c704846a82da 100644 --- a/compiler/src/dotty/tools/dotc/core/Phases.scala +++ b/compiler/src/dotty/tools/dotc/core/Phases.scala @@ -333,16 +333,25 @@ object Phases { def subPhases: List[Run.SubPhase] = Nil final def traversals: Int = if subPhases.isEmpty then 1 else subPhases.length + /** skip the phase for a Java compilation unit, may depend on -Yjava-tasty */ + def skipIfJava(using Context): Boolean = true + /** @pre `isRunnable` returns true */ def run(using Context): Unit /** @pre `isRunnable` returns true */ def runOn(units: List[CompilationUnit])(using runCtx: Context): List[CompilationUnit] = val buf = List.newBuilder[CompilationUnit] + // factor out typedAsJava check when not needed + val doSkipJava = ctx.settings.YjavaTasty.value && this <= picklerPhase && skipIfJava for unit <- units do given unitCtx: Context = runCtx.fresh.setPhase(this.start).setCompilationUnit(unit).withRootImports if ctx.run.enterUnit(unit) then - try run + try + if doSkipJava && unit.typedAsJava then + () + else + run catch case ex: Throwable if !ctx.run.enrichedErrorMessage => println(ctx.run.enrichErrorMessage(s"unhandled exception while running $phaseName on $unit")) throw ex diff --git a/compiler/src/dotty/tools/dotc/core/tasty/Attributes.scala b/compiler/src/dotty/tools/dotc/core/tasty/Attributes.scala index 7f87b7f4f2b7..0697f39f6dab 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/Attributes.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/Attributes.scala @@ -11,6 +11,8 @@ class Attributes private[tasty]( def explicitNulls: Boolean = booleanTags(EXPLICITNULLSattr) def captureChecked: Boolean = booleanTags(CAPTURECHECKEDattr) def withPureFuns: Boolean = booleanTags(WITHPUREFUNSattr) + def isJava: Boolean = booleanTags(JAVAattr) + def isOutline: Boolean = booleanTags(OUTLINEattr) } object Attributes: @@ -19,12 +21,16 @@ object Attributes: explicitNulls: Boolean, captureChecked: Boolean, withPureFuns: Boolean, + isJava: Boolean, + isOutline: Boolean, ): Attributes = val booleanTags = BitSet.newBuilder if scala2StandardLibrary then booleanTags += SCALA2STANDARDLIBRARYattr if explicitNulls then booleanTags += EXPLICITNULLSattr if captureChecked then booleanTags += CAPTURECHECKEDattr if withPureFuns then booleanTags += WITHPUREFUNSattr + if isJava then booleanTags += JAVAattr + if isOutline then booleanTags += OUTLINEattr new Attributes(booleanTags.result()) end apply diff --git a/compiler/src/dotty/tools/dotc/core/tasty/DottyUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/DottyUnpickler.scala index 033d8cd40c1f..8753017cc82f 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/DottyUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/DottyUnpickler.scala @@ -36,6 +36,7 @@ object DottyUnpickler { def unpickle(reader: TastyReader, nameAtRef: NameTable): CommentUnpickler = new CommentUnpickler(reader) } + class AttributesSectionUnpickler extends SectionUnpickler[AttributeUnpickler](AttributesSection) { def unpickle(reader: TastyReader, nameAtRef: NameTable): AttributeUnpickler = new AttributeUnpickler(reader) diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala index b4ba58c55c93..dafd6c2e8daa 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala @@ -23,7 +23,7 @@ import quoted.QuotePatterns object TreePickler: class StackSizeExceeded(val mdef: tpd.MemberDef) extends Exception -class TreePickler(pickler: TastyPickler) { +class TreePickler(pickler: TastyPickler, attributes: Attributes) { val buf: TreeBuffer = new TreeBuffer pickler.newSection(ASTsSection, buf) import buf.* @@ -322,6 +322,11 @@ class TreePickler(pickler: TastyPickler) { if (!tree.isEmpty) pickleTree(tree) } + def pickleElidedUnlessEmpty(tree: Tree, tp: Type)(using Context): Unit = + if !tree.isEmpty then + writeByte(ELIDED) + pickleType(tp) + def pickleDef(tag: Int, mdef: MemberDef, tpt: Tree, rhs: Tree = EmptyTree, pickleParams: => Unit = ())(using Context): Unit = { val sym = mdef.symbol @@ -337,7 +342,12 @@ class TreePickler(pickler: TastyPickler) { case _: Template | _: Hole => pickleTree(tpt) case _ if tpt.isType => pickleTpt(tpt) } - pickleTreeUnlessEmpty(rhs) + if attributes.isOutline && sym.isTerm && attributes.isJava then + // TODO: if we introduce outline typing for Scala definitions + // then we will need to update the check here + pickleElidedUnlessEmpty(rhs, tpt.tpe) + else + pickleTreeUnlessEmpty(rhs) pickleModifiers(sym, mdef) } catch diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index 1f551028c415..d4271d5bffaf 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -104,6 +104,11 @@ class TreeUnpickler(reader: TastyReader, private val explicitNulls = attributeUnpicklerOpt.exists(_.attributes.explicitNulls) + private val unpicklingJava = + attributeUnpicklerOpt.exists(_.attributes.isJava) + + private val isOutline = attributeUnpicklerOpt.exists(_.attributes.isOutline) + private def registerSym(addr: Addr, sym: Symbol) = symAtAddr(addr) = sym @@ -609,7 +614,10 @@ class TreeUnpickler(reader: TastyReader, val rhsIsEmpty = nothingButMods(end) if (!rhsIsEmpty) skipTree() val (givenFlags0, annotFns, privateWithin) = readModifiers(end) - val givenFlags = if isClass && unpicklingScala2Library then givenFlags0 | Scala2x | Scala2Tasty else givenFlags0 + val givenFlags = + if isClass && unpicklingScala2Library then givenFlags0 | Scala2x | Scala2Tasty + else if unpicklingJava then givenFlags0 | JavaDefined + else givenFlags0 pickling.println(i"creating symbol $name at $start with flags ${givenFlags.flagsString}, isAbsType = $isAbsType, $ttag") val flags = normalizeFlags(tag, givenFlags, name, isAbsType, rhsIsEmpty) def adjustIfModule(completer: LazyType) = @@ -1037,6 +1045,8 @@ class TreeUnpickler(reader: TastyReader, val parentReader = fork val parents = readParents(withArgs = false)(using parentCtx) val parentTypes = parents.map(_.tpe.dealias) + if cls.is(JavaDefined) && parentTypes.exists(_.derivesFrom(defn.JavaAnnotationClass)) then + cls.setFlag(JavaAnnotation) val self = if (nextByte == SELFDEF) { readByte() @@ -1197,7 +1207,12 @@ class TreeUnpickler(reader: TastyReader, def completeSelect(name: Name, sig: Signature, target: Name): Select = val qual = readTree() - val denot = accessibleDenot(qual.tpe.widenIfUnstable, name, sig, target) + val denot0 = accessibleDenot(qual.tpe.widenIfUnstable, name, sig, target) + val denot = + if unpicklingJava && name == tpnme.Object && denot0.symbol == defn.ObjectClass then + defn.FromJavaObjectType.denot + else + denot0 makeSelect(qual, name, denot) def readQualId(): (untpd.Ident, TypeRef) = @@ -1216,6 +1231,11 @@ class TreeUnpickler(reader: TastyReader, forkAt(readAddr()).readTree() case IDENT => untpd.Ident(readName()).withType(readType()) + case ELIDED => + if !isOutline then + report.error( + s"Illegal elided tree in unpickler without ${attributeTagToString(OUTLINEattr)}, ${ctx.source}") + untpd.Ident(nme.WILDCARD).withType(readType()) case IDENTtpt => untpd.Ident(readName().toTypeName).withType(readType()) case SELECT => diff --git a/compiler/src/dotty/tools/dotc/fromtasty/ReadTasty.scala b/compiler/src/dotty/tools/dotc/fromtasty/ReadTasty.scala index 4969882b7766..9cf2421c3bda 100644 --- a/compiler/src/dotty/tools/dotc/fromtasty/ReadTasty.scala +++ b/compiler/src/dotty/tools/dotc/fromtasty/ReadTasty.scala @@ -46,9 +46,16 @@ class ReadTasty extends Phase { case unpickler: tasty.DottyUnpickler => if (cls.rootTree.isEmpty) None else { - val unit = CompilationUnit(cls, cls.rootTree, forceTrees = true) - unit.pickled += (cls -> (() => unpickler.unpickler.bytes)) - Some(unit) + val attributes = unpickler.tastyAttributes + if attributes.isJava && !ctx.settings.YjavaTasty.value then + // filter out Java compilation units if -Yjava-tasty is not set + None + else if attributes.isOutline && !ctx.settings.YallowOutlineFromTasty.value then + cannotUnpickle("it contains outline signatures and -Yallow-outline-from-tasty is not set.") + else + val unit = CompilationUnit(cls, cls.rootTree, forceTrees = true) + unit.pickled += (cls -> (() => unpickler.unpickler.bytes)) + Some(unit) } case tree: Tree[?] => // TODO handle correctly this case correctly to get the tree or avoid it completely. diff --git a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala index 1c1c47ad68ce..bdd29d9ec0ef 100644 --- a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala @@ -136,7 +136,7 @@ object JavaParsers { ValDef(name, tpt, EmptyTree).withMods(Modifiers(Flags.JavaDefined | Flags.Param)) def makeConstructor(formals: List[Tree], tparams: List[TypeDef], flags: FlagSet = Flags.JavaDefined): DefDef = { - val vparams = formals.zipWithIndex.map { case (p, i) => makeSyntheticParam(i + 1, p) } + val vparams = formals.zipWithIndex.map { case (p, i) => makeSyntheticParam(i + 1, p).withMods(Modifiers(flags)) } DefDef(nme.CONSTRUCTOR, joinParams(tparams, List(vparams)), TypeTree(), EmptyTree).withMods(Modifiers(flags)) } diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 306656b137c8..f3a2300913ec 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -97,6 +97,9 @@ object Parsers { private val InCond: Region => Region = Scanners.InParens(LPAREN, _) private val InFor : Region => Region = Scanners.InBraces(_) + def unimplementedExpr(using Context): Select = + Select(scalaDot(nme.Predef), nme.???) + abstract class ParserCommon(val source: SourceFile)(using Context) { val in: ScannerCommon @@ -164,9 +167,6 @@ object Parsers { */ def syntaxError(msg: Message, span: Span): Unit = report.error(msg, source.atSpan(span)) - - def unimplementedExpr(using Context): Select = - Select(scalaDot(nme.Predef), nme.???) } trait OutlineParserCommon extends ParserCommon { diff --git a/compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala b/compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala index a9b66fc056e2..6a030c424f08 100644 --- a/compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala +++ b/compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala @@ -9,7 +9,7 @@ import dotty.tools.dotc.core.Flags.* import dotty.tools.dotc.core.Mode import dotty.tools.dotc.core.Symbols.* import dotty.tools.dotc.core.Types.* -import dotty.tools.dotc.core.tasty.{ PositionPickler, TastyPickler, TastyPrinter, TreePickler } +import dotty.tools.dotc.core.tasty.{ PositionPickler, TastyPickler, TastyPrinter, TreePickler, Attributes } import dotty.tools.dotc.core.tasty.DottyUnpickler import dotty.tools.dotc.core.tasty.TreeUnpickler.UnpickleMode import dotty.tools.dotc.report @@ -217,7 +217,7 @@ object PickledQuotes { private def pickle(tree: Tree)(using Context): Array[Byte] = { quotePickling.println(i"**** pickling quote of\n$tree") val pickler = new TastyPickler(defn.RootClass) - val treePkl = new TreePickler(pickler) + val treePkl = new TreePickler(pickler, Attributes.empty) treePkl.pickle(tree :: Nil) treePkl.compactify() if tree.span.exists then diff --git a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala index bb1d2afd04d6..dafb44d525e4 100644 --- a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala +++ b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala @@ -54,6 +54,9 @@ class ExtractAPI extends Phase { // Check no needed. Does not transform trees override def isCheckable: Boolean = false + // when `-Yjava-tasty` is set we actually want to run this phase on Java sources + override def skipIfJava(using Context): Boolean = false + // SuperAccessors need to be part of the API (see the scripted test // `trait-super` for an example where this matters), this is only the case // after `PostTyper` (unlike `ExtractDependencies`, the simplication to trees diff --git a/compiler/src/dotty/tools/dotc/sbt/ExtractDependencies.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractDependencies.scala index 36982633e881..a35628dc52e4 100644 --- a/compiler/src/dotty/tools/dotc/sbt/ExtractDependencies.scala +++ b/compiler/src/dotty/tools/dotc/sbt/ExtractDependencies.scala @@ -64,6 +64,9 @@ class ExtractDependencies extends Phase { // Check no needed. Does not transform trees override def isCheckable: Boolean = false + // when `-Yjava-tasty` is set we actually want to run this phase on Java sources + override def skipIfJava(using Context): Boolean = false + // This phase should be run directly after `Frontend`, if it is run after // `PostTyper`, some dependencies will be lost because trees get simplified. // See the scripted test `constants` for an example where this matters. diff --git a/compiler/src/dotty/tools/dotc/transform/Pickler.scala b/compiler/src/dotty/tools/dotc/transform/Pickler.scala index c361b50af819..2dc1599365cd 100644 --- a/compiler/src/dotty/tools/dotc/transform/Pickler.scala +++ b/compiler/src/dotty/tools/dotc/transform/Pickler.scala @@ -9,6 +9,8 @@ import tasty.* import config.Printers.{noPrinter, pickling} import config.Feature import java.io.PrintStream +import io.ClassfileWriterOps +import StdNames.str import Periods.* import Phases.* import Symbols.* @@ -17,6 +19,7 @@ import reporting.{ThrowingReporter, Profile, Message} import collection.mutable import util.concurrent.{Executor, Future} import compiletime.uninitialized +import dotty.tools.io.JarArchive object Pickler { val name: String = "pickler" @@ -27,6 +30,9 @@ object Pickler { * only in backend. */ inline val ParallelPickling = true + + class EarlyFileWriter(writer: ClassfileWriterOps): + export writer.{writeTasty, close} } /** This phase pickles trees */ @@ -39,7 +45,10 @@ class Pickler extends Phase { // No need to repickle trees coming from TASTY override def isRunnable(using Context): Boolean = - super.isRunnable && !ctx.settings.fromTasty.value + super.isRunnable && (!ctx.settings.fromTasty.value || ctx.settings.YjavaTasty.value) + + // when `-Yjava-tasty` is set we actually want to run this phase on Java sources + override def skipIfJava(using Context): Boolean = false private def output(name: String, msg: String) = { val s = new PrintStream(name) @@ -74,7 +83,8 @@ class Pickler extends Phase { private val executor = Executor[Array[Byte]]() private def useExecutor(using Context) = - Pickler.ParallelPickling && !ctx.settings.YtestPickler.value + Pickler.ParallelPickling && !ctx.settings.YtestPickler.value && + !ctx.settings.YjavaTasty.value // disable parallel pickling when `-Yjava-tasty` is set (internal testing only) override def run(using Context): Unit = { val unit = ctx.compilationUnit @@ -86,8 +96,22 @@ class Pickler extends Phase { do if ctx.settings.YtestPickler.value then beforePickling(cls) = tree.show + val isJavaAttr = unit.isJava // we must always set JAVAattr when pickling Java sources + if isJavaAttr then + // assert that Java sources didn't reach Pickler without `-Yjava-tasty`. + assert(ctx.settings.YjavaTasty.value, "unexpected Java source file without -Yjava-tasty") + val isOutline = isJavaAttr // TODO: later we may want outline for Scala sources too + val attributes = Attributes( + scala2StandardLibrary = ctx.settings.YcompileScala2Library.value, + explicitNulls = ctx.settings.YexplicitNulls.value, + captureChecked = Feature.ccEnabled, + withPureFuns = Feature.pureFunsEnabled, + isJava = isJavaAttr, + isOutline = isOutline + ) + val pickler = new TastyPickler(cls) - val treePkl = new TreePickler(pickler) + val treePkl = new TreePickler(pickler, attributes) treePkl.pickle(tree :: Nil) Profile.current.recordTasty(treePkl.buf.length) @@ -109,12 +133,6 @@ class Pickler extends Phase { pickler, treePkl.buf.addrOfTree, treePkl.docString, tree, scratch.commentBuffer) - val attributes = Attributes( - scala2StandardLibrary = ctx.settings.YcompileScala2Library.value, - explicitNulls = ctx.settings.YexplicitNulls.value, - captureChecked = Feature.ccEnabled, - withPureFuns = Feature.pureFunsEnabled, - ) AttributePickler.pickleAttributes(attributes, pickler, scratch.attributeBuffer) val pickled = pickler.assembleParts() @@ -154,13 +172,22 @@ class Pickler extends Phase { } override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] = { - val result = - if useExecutor then - executor.start() - try super.runOn(units) - finally executor.close() + val sigWriter: Option[Pickler.EarlyFileWriter] = ctx.settings.YjavaTastyOutput.value match + case jar: JarArchive if jar.exists => + Some(Pickler.EarlyFileWriter(ClassfileWriterOps(jar))) + case _ => + None + val units0 = + if ctx.settings.fromTasty.value then + // we still run the phase for the side effect of writing the pipeline tasty files + units else - super.runOn(units) + if useExecutor then + executor.start() + try super.runOn(units) + finally executor.close() + else + super.runOn(units) if ctx.settings.YtestPickler.value then val ctx2 = ctx.fresh .setSetting(ctx.settings.YreadComments, true) @@ -171,9 +198,34 @@ class Pickler extends Phase { .setReporter(new ThrowingReporter(ctx.reporter)) .addMode(Mode.ReadPositions) ) + val result = + if ctx.settings.YjavaTasty.value then + sigWriter.foreach(writeJavaSigFiles(units0, _)) + units0.filterNot(_.typedAsJava) // remove java sources, this is the terminal phase when `-Yjava-tasty` is set + else + units0 result } + private def writeJavaSigFiles(units: List[CompilationUnit], writer: Pickler.EarlyFileWriter)(using Context): Unit = { + var count = 0 + try + for + unit <- units if unit.typedAsJava + (cls, pickled) <- unit.pickled + if cls.isDefinedInCurrentRun + do + val binaryName = cls.binaryClassName.replace('.', java.io.File.separatorChar).nn + val binaryClassName = if (cls.is(Module)) binaryName.stripSuffix(str.MODULE_SUFFIX).nn else binaryName + writer.writeTasty(binaryClassName, pickled()) + count += 1 + finally + writer.close() + if ctx.settings.verbose.value then + report.echo(s"[$count java sig files written]") + end try + } + private def testUnpickler(using Context): Unit = pickling.println(i"testing unpickler at run ${ctx.runId}") ctx.initialize() diff --git a/compiler/src/dotty/tools/dotc/typer/TyperPhase.scala b/compiler/src/dotty/tools/dotc/typer/TyperPhase.scala index b79235f4f819..b16447ecb15b 100644 --- a/compiler/src/dotty/tools/dotc/typer/TyperPhase.scala +++ b/compiler/src/dotty/tools/dotc/typer/TyperPhase.scala @@ -57,7 +57,7 @@ class TyperPhase(addRootImports: Boolean = true) extends Phase { } protected def discardAfterTyper(unit: CompilationUnit)(using Context): Boolean = - unit.isJava || unit.suspended + (unit.isJava && !ctx.settings.YjavaTasty.value) || unit.suspended override val subPhases: List[SubPhase] = List( SubPhase("indexing"), SubPhase("typechecking"), SubPhase("checkingJava")) diff --git a/compiler/src/dotty/tools/io/ClassfileWriterOps.scala b/compiler/src/dotty/tools/io/ClassfileWriterOps.scala new file mode 100644 index 000000000000..c2107ded6f51 --- /dev/null +++ b/compiler/src/dotty/tools/io/ClassfileWriterOps.scala @@ -0,0 +1,50 @@ +package dotty.tools.io + +import dotty.tools.io.* +import dotty.tools.dotc.core.Decorators.* +import dotty.tools.dotc.core.Contexts.* +import dotty.tools.dotc.report +import scala.language.unsafeNulls +import scala.annotation.constructorOnly + + +/** Experimental usage - writes bytes to JarArchives */ +class ClassfileWriterOps(outputDir: JarArchive)(using @constructorOnly ictx: Context) { + + type InternalName = String + + // if non-null, classfiles are written to a jar instead of the output directory + private val jarWriter: JarWriter | Null = + val localCtx = ictx + outputDir.underlyingSource.map { source => + if outputDir.isEmpty then + new Jar(source.file).jarWriter() + else inContext(localCtx) { + // Writing to non-empty JAR might be an undefined behaviour, e.g. in case if other files where + // created using `AbstractFile.bufferedOutputStream`instead of JarWriter + report.warning(em"Tried to write to non-empty JAR: $source") + null + } + }.getOrElse( + inContext(localCtx) { + report.warning(em"tried to create a file writer for $outputDir, but it had no underlying source.") + null + } + ) + + def writeTasty(className: InternalName, bytes: Array[Byte]): Unit = + writeToJar(className, bytes, ".tasty") + + private def writeToJar(className: InternalName, bytes: Array[Byte], suffix: String): Unit = { + if (jarWriter == null) return + val path = className + suffix + val out = jarWriter.newOutputStream(path) + try out.write(bytes, 0, bytes.length) + finally out.flush() + } + + def close(): Unit = { + if (jarWriter != null) jarWriter.close() + outputDir.close() + } +} diff --git a/compiler/src/dotty/tools/io/JarArchive.scala b/compiler/src/dotty/tools/io/JarArchive.scala index 49b743e83074..f42f68e745ed 100644 --- a/compiler/src/dotty/tools/io/JarArchive.scala +++ b/compiler/src/dotty/tools/io/JarArchive.scala @@ -12,6 +12,7 @@ import scala.jdk.CollectionConverters.* */ class JarArchive private (root: Directory) extends PlainDirectory(root) { def close(): Unit = jpath.getFileSystem().close() + override def exists: Boolean = jpath.getFileSystem().isOpen() && super.exists def allFileNames(): Iterator[String] = java.nio.file.Files.walk(jpath).iterator().asScala.map(_.toString) } diff --git a/sbt-test/pipelining/Yjava-tasty-annotation/a/src/main/scala/a/A.java b/sbt-test/pipelining/Yjava-tasty-annotation/a/src/main/scala/a/A.java new file mode 100644 index 000000000000..49c55a7c4d9c --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-annotation/a/src/main/scala/a/A.java @@ -0,0 +1,11 @@ +// this test ensures that it is possible to read a java annotation from TASTy. +package a; + +import java.lang.annotation.*; + + +@Documented +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.TYPE, ElementType.PACKAGE }) +public @interface A { +} diff --git a/sbt-test/pipelining/Yjava-tasty-annotation/a/src/main/scala/a/package.scala b/sbt-test/pipelining/Yjava-tasty-annotation/a/src/main/scala/a/package.scala new file mode 100644 index 000000000000..93f99e9892fe --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-annotation/a/src/main/scala/a/package.scala @@ -0,0 +1,2 @@ +// THIS FILE EXISTS SO THAT `A.java` WILL BE COMPILED BY SCALAC +package a diff --git a/sbt-test/pipelining/Yjava-tasty-annotation/b/src/main/scala/b/B.scala b/sbt-test/pipelining/Yjava-tasty-annotation/b/src/main/scala/b/B.scala new file mode 100644 index 000000000000..51c7322bf264 --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-annotation/b/src/main/scala/b/B.scala @@ -0,0 +1,9 @@ +package b + +import a.A + +object B { + @A + val foo = 23 +} + diff --git a/sbt-test/pipelining/Yjava-tasty-annotation/build.sbt b/sbt-test/pipelining/Yjava-tasty-annotation/build.sbt new file mode 100644 index 000000000000..28b9b0e2bee9 --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-annotation/build.sbt @@ -0,0 +1,13 @@ +lazy val a = project.in(file("a")) + .settings( + scalacOptions += "-Yjava-tasty", // enable pickling of java signatures + scalacOptions ++= Seq("-Yjava-tasty-output", ((ThisBuild / baseDirectory).value / "a-annotation-java-tasty.jar").toString), + scalacOptions += "-Ycheck:all", + classDirectory := ((ThisBuild / baseDirectory).value / "a-annotation-classes"), // send classfiles to a different directory + ) + +lazy val b = project.in(file("b")) + .settings( + Compile / unmanagedClasspath := Seq(Attributed.blank((ThisBuild / baseDirectory).value / "a-annotation-java-tasty.jar")), + scalacOptions += "-Ycheck:all", + ) diff --git a/sbt-test/pipelining/Yjava-tasty-annotation/project/DottyInjectedPlugin.scala b/sbt-test/pipelining/Yjava-tasty-annotation/project/DottyInjectedPlugin.scala new file mode 100644 index 000000000000..69f15d168bfc --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-annotation/project/DottyInjectedPlugin.scala @@ -0,0 +1,12 @@ +import sbt._ +import Keys._ + +object DottyInjectedPlugin extends AutoPlugin { + override def requires = plugins.JvmPlugin + override def trigger = allRequirements + + override val projectSettings = Seq( + scalaVersion := sys.props("plugin.scalaVersion"), + scalacOptions += "-source:3.0-migration" + ) +} diff --git a/sbt-test/pipelining/Yjava-tasty-annotation/test b/sbt-test/pipelining/Yjava-tasty-annotation/test new file mode 100644 index 000000000000..6105296d455b --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-annotation/test @@ -0,0 +1,3 @@ +> a/compile +# Test depending on a java compiled annotation through TASTy +> b/compile diff --git a/sbt-test/pipelining/Yjava-tasty-enum/a/src/main/scala/a/A.java b/sbt-test/pipelining/Yjava-tasty-enum/a/src/main/scala/a/A.java new file mode 100644 index 000000000000..26bf8a246774 --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-enum/a/src/main/scala/a/A.java @@ -0,0 +1,7 @@ +// this test ensures that ExtractAPI does not cause a crash +// when looking at sealedDescendants of a Java enum. +package a; + +public enum A { + X, Y, Z; +} diff --git a/sbt-test/pipelining/Yjava-tasty-enum/a/src/main/scala/a/package.scala b/sbt-test/pipelining/Yjava-tasty-enum/a/src/main/scala/a/package.scala new file mode 100644 index 000000000000..93f99e9892fe --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-enum/a/src/main/scala/a/package.scala @@ -0,0 +1,2 @@ +// THIS FILE EXISTS SO THAT `A.java` WILL BE COMPILED BY SCALAC +package a diff --git a/sbt-test/pipelining/Yjava-tasty-enum/b/src/main/scala/b/B.scala b/sbt-test/pipelining/Yjava-tasty-enum/b/src/main/scala/b/B.scala new file mode 100644 index 000000000000..a648bb4e83d6 --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-enum/b/src/main/scala/b/B.scala @@ -0,0 +1,17 @@ +package b + +import a.A + +object B { + + def formattedEnum(e: A): String = e match { + case A.X => "X" + case A.Y => "Y" + case A.Z => "Z" + } + + @main def test = + assert(A.values.toList == List(A.X, A.Y, A.Z)) + assert(A.values.toList.map(formattedEnum) == List("X", "Y", "Z")) +} + diff --git a/sbt-test/pipelining/Yjava-tasty-enum/build.sbt b/sbt-test/pipelining/Yjava-tasty-enum/build.sbt new file mode 100644 index 000000000000..1c416c65896f --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-enum/build.sbt @@ -0,0 +1,15 @@ +lazy val a = project.in(file("a")) + .settings( + compileOrder := CompileOrder.Mixed, // ensure we send java sources to Scala compiler + scalacOptions += "-Yjava-tasty", // enable pickling of java signatures + scalacOptions ++= Seq("-Yjava-tasty-output", ((ThisBuild / baseDirectory).value / "a-enum-java-tasty.jar").toString), + scalacOptions += "-Ycheck:all", + classDirectory := ((ThisBuild / baseDirectory).value / "a-enum-classes"), // send classfiles to a different directory + ) + + +lazy val b = project.in(file("b")) + .settings( + Compile / unmanagedClasspath := Seq(Attributed.blank((ThisBuild / baseDirectory).value / "a-enum-java-tasty.jar")), + scalacOptions += "-Ycheck:all", + ) diff --git a/sbt-test/pipelining/Yjava-tasty-enum/project/DottyInjectedPlugin.scala b/sbt-test/pipelining/Yjava-tasty-enum/project/DottyInjectedPlugin.scala new file mode 100644 index 000000000000..69f15d168bfc --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-enum/project/DottyInjectedPlugin.scala @@ -0,0 +1,12 @@ +import sbt._ +import Keys._ + +object DottyInjectedPlugin extends AutoPlugin { + override def requires = plugins.JvmPlugin + override def trigger = allRequirements + + override val projectSettings = Seq( + scalaVersion := sys.props("plugin.scalaVersion"), + scalacOptions += "-source:3.0-migration" + ) +} diff --git a/sbt-test/pipelining/Yjava-tasty-enum/test b/sbt-test/pipelining/Yjava-tasty-enum/test new file mode 100644 index 000000000000..68e3c170d3b5 --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-enum/test @@ -0,0 +1,3 @@ +> a/compile +# test depending on a java compiled enum through TASTy +> b/compile diff --git a/sbt-test/pipelining/Yjava-tasty-from-tasty/a/src/main/scala/a/A.java b/sbt-test/pipelining/Yjava-tasty-from-tasty/a/src/main/scala/a/A.java new file mode 100644 index 000000000000..b8a278edc32f --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-from-tasty/a/src/main/scala/a/A.java @@ -0,0 +1,5 @@ +package a; + +public class A { + public static final String VALUE = "A"; +} diff --git a/sbt-test/pipelining/Yjava-tasty-from-tasty/a/src/main/scala/a/package.scala b/sbt-test/pipelining/Yjava-tasty-from-tasty/a/src/main/scala/a/package.scala new file mode 100644 index 000000000000..8cfc7fa44d87 --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-from-tasty/a/src/main/scala/a/package.scala @@ -0,0 +1,2 @@ +// THE PURPOSE OF THIS FILE IS TO MAKE SBT SEND A.java TO THE SCALA COMPILER +package a diff --git a/sbt-test/pipelining/Yjava-tasty-from-tasty/a_from_tasty/.keep b/sbt-test/pipelining/Yjava-tasty-from-tasty/a_from_tasty/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/sbt-test/pipelining/Yjava-tasty-from-tasty/b/src/main/scala/b/B.scala b/sbt-test/pipelining/Yjava-tasty-from-tasty/b/src/main/scala/b/B.scala new file mode 100644 index 000000000000..884bf1a927ff --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-from-tasty/b/src/main/scala/b/B.scala @@ -0,0 +1,5 @@ +package b + +object B { + val A: "A" = a.A.VALUE +} diff --git a/sbt-test/pipelining/Yjava-tasty-from-tasty/build.sbt b/sbt-test/pipelining/Yjava-tasty-from-tasty/build.sbt new file mode 100644 index 000000000000..dc4950ec8379 --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-from-tasty/build.sbt @@ -0,0 +1,29 @@ +// `a` contains mixed java/scala sources so sbt will send java sources to Scala compiler. +lazy val a = project.in(file("a")) + .settings( + compileOrder := CompileOrder.Mixed, // ensure we send java sources to Scala compiler + scalacOptions += "-Yjava-tasty", // enable pickling of java signatures + scalacOptions ++= Seq("-Yjava-tasty-output", ((ThisBuild / baseDirectory).value / "a-pre-java-tasty.jar").toString), + scalacOptions += "-Ycheck:all", + classDirectory := ((ThisBuild / baseDirectory).value / "a-pre-classes"), // send classfiles to a different directory + ) + +// recompile `a` with `-from-tasty` flag to test idempotent read/write java signatures. +// Requires -Yjava-tasty to be set in order to read them. +lazy val a_from_tasty = project.in(file("a_from_tasty")) + .settings( + Compile / sources := Seq((ThisBuild / baseDirectory).value / "a-pre-java-tasty.jar"), + Compile / unmanagedClasspath := Seq(Attributed.blank((ThisBuild / baseDirectory).value / "a-pre-java-tasty.jar")), + scalacOptions += "-from-tasty", // read the jar file tasties as the source files + scalacOptions += "-Yjava-tasty", + scalacOptions += "-Yallow-outline-from-tasty", // allow outline signatures to be read with -from-tasty + scalacOptions ++= Seq("-Yjava-tasty-output", ((ThisBuild / baseDirectory).value / "a_from_tasty-java-tasty.jar").toString), + scalacOptions += "-Ycheck:all", + classDirectory := ((ThisBuild / baseDirectory).value / "a_from_tasty-classes"), // send classfiles to a different directory + ) + +lazy val b = project.in(file("b")) + .settings( + Compile / unmanagedClasspath := Seq(Attributed.blank((ThisBuild / baseDirectory).value / "a_from_tasty-java-tasty.jar")), + scalacOptions += "-Ycheck:all", + ) diff --git a/sbt-test/pipelining/Yjava-tasty-from-tasty/project/DottyInjectedPlugin.scala b/sbt-test/pipelining/Yjava-tasty-from-tasty/project/DottyInjectedPlugin.scala new file mode 100644 index 000000000000..69f15d168bfc --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-from-tasty/project/DottyInjectedPlugin.scala @@ -0,0 +1,12 @@ +import sbt._ +import Keys._ + +object DottyInjectedPlugin extends AutoPlugin { + override def requires = plugins.JvmPlugin + override def trigger = allRequirements + + override val projectSettings = Seq( + scalaVersion := sys.props("plugin.scalaVersion"), + scalacOptions += "-source:3.0-migration" + ) +} diff --git a/sbt-test/pipelining/Yjava-tasty-from-tasty/test b/sbt-test/pipelining/Yjava-tasty-from-tasty/test new file mode 100644 index 000000000000..af7eced8e846 --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-from-tasty/test @@ -0,0 +1,5 @@ +> a/compile +# test reading java tasty with -from-tasty +> a_from_tasty/compile +# test java tasty is still written even with -from-tasty +> b/compile diff --git a/sbt-test/pipelining/Yjava-tasty-generic/a/src/main/scala/a/A.java b/sbt-test/pipelining/Yjava-tasty-generic/a/src/main/scala/a/A.java new file mode 100644 index 000000000000..1fcb7e78ae3d --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-generic/a/src/main/scala/a/A.java @@ -0,0 +1,14 @@ +// this test ensures that it is possible to read a generic java class from TASTy. +package a; + +public abstract class A { + private final int _value; + + protected A(final int value) { + this._value = value; + } + + public int value() { + return _value; + } +} diff --git a/sbt-test/pipelining/Yjava-tasty-generic/a/src/main/scala/a/package.scala b/sbt-test/pipelining/Yjava-tasty-generic/a/src/main/scala/a/package.scala new file mode 100644 index 000000000000..93f99e9892fe --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-generic/a/src/main/scala/a/package.scala @@ -0,0 +1,2 @@ +// THIS FILE EXISTS SO THAT `A.java` WILL BE COMPILED BY SCALAC +package a diff --git a/sbt-test/pipelining/Yjava-tasty-generic/b/src/main/scala/b/B.scala b/sbt-test/pipelining/Yjava-tasty-generic/b/src/main/scala/b/B.scala new file mode 100644 index 000000000000..dcb4935860df --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-generic/b/src/main/scala/b/B.scala @@ -0,0 +1,12 @@ +package b + +import a.A + +class B[T] { + val inner = new A[T](23) {} +} + +object B { + val derived: Int = (new B[Int]).inner.value +} + diff --git a/sbt-test/pipelining/Yjava-tasty-generic/build.sbt b/sbt-test/pipelining/Yjava-tasty-generic/build.sbt new file mode 100644 index 000000000000..aa5d3099e979 --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-generic/build.sbt @@ -0,0 +1,13 @@ +lazy val a = project.in(file("a")) + .settings( + scalacOptions += "-Yjava-tasty", // enable pickling of java signatures + scalacOptions ++= Seq("-Yjava-tasty-output", ((ThisBuild / baseDirectory).value / "a-generic-java-tasty.jar").toString), + scalacOptions += "-Ycheck:all", + classDirectory := ((ThisBuild / baseDirectory).value / "a-generic-classes"), // send classfiles to a different directory + ) + +lazy val b = project.in(file("b")) + .settings( + Compile / unmanagedClasspath := Seq(Attributed.blank((ThisBuild / baseDirectory).value / "a-generic-java-tasty.jar")), + scalacOptions += "-Ycheck:all", + ) diff --git a/sbt-test/pipelining/Yjava-tasty-generic/project/DottyInjectedPlugin.scala b/sbt-test/pipelining/Yjava-tasty-generic/project/DottyInjectedPlugin.scala new file mode 100644 index 000000000000..69f15d168bfc --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-generic/project/DottyInjectedPlugin.scala @@ -0,0 +1,12 @@ +import sbt._ +import Keys._ + +object DottyInjectedPlugin extends AutoPlugin { + override def requires = plugins.JvmPlugin + override def trigger = allRequirements + + override val projectSettings = Seq( + scalaVersion := sys.props("plugin.scalaVersion"), + scalacOptions += "-source:3.0-migration" + ) +} diff --git a/sbt-test/pipelining/Yjava-tasty-generic/test b/sbt-test/pipelining/Yjava-tasty-generic/test new file mode 100644 index 000000000000..5abac4b5eae7 --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-generic/test @@ -0,0 +1,3 @@ +> a/compile +# Test depending on a java generic class through TASTy +> b/compile diff --git a/sbt-test/pipelining/Yjava-tasty-result-types/a/src/main/scala/a/A.java b/sbt-test/pipelining/Yjava-tasty-result-types/a/src/main/scala/a/A.java new file mode 100644 index 000000000000..c48f149849a6 --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-result-types/a/src/main/scala/a/A.java @@ -0,0 +1,9 @@ +package a; + +public class A { + public static final String VALUE = "A"; + + public String add(T t) { + return VALUE + t.toString(); + } +} diff --git a/sbt-test/pipelining/Yjava-tasty-result-types/a/src/main/scala/a/package.scala b/sbt-test/pipelining/Yjava-tasty-result-types/a/src/main/scala/a/package.scala new file mode 100644 index 000000000000..93f99e9892fe --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-result-types/a/src/main/scala/a/package.scala @@ -0,0 +1,2 @@ +// THIS FILE EXISTS SO THAT `A.java` WILL BE COMPILED BY SCALAC +package a diff --git a/sbt-test/pipelining/Yjava-tasty-result-types/b/src/main/scala/b/B.scala b/sbt-test/pipelining/Yjava-tasty-result-types/b/src/main/scala/b/B.scala new file mode 100644 index 000000000000..b67840f2f852 --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-result-types/b/src/main/scala/b/B.scala @@ -0,0 +1,17 @@ +package b + +import a.A + +object B { + val finalResult: "A" = A.VALUE + + val a_B: String = (new A()).add("B") + val a_true: String = (new A()).add(true) + + @main def test = { + assert(finalResult == "A") + assert(a_B == "AB") + assert(a_true == "Atrue") + } +} + diff --git a/sbt-test/pipelining/Yjava-tasty-result-types/build.sbt b/sbt-test/pipelining/Yjava-tasty-result-types/build.sbt new file mode 100644 index 000000000000..f9cf8082c731 --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-result-types/build.sbt @@ -0,0 +1,13 @@ +lazy val a = project.in(file("a")) + .settings( + scalacOptions += "-Yjava-tasty", // enable pickling of java signatures + scalacOptions ++= Seq("-Yjava-tasty-output", ((ThisBuild / baseDirectory).value / "a-result-types-java-tasty.jar").toString), + scalacOptions += "-Ycheck:all", + classDirectory := ((ThisBuild / baseDirectory).value / "a-result-types-classes"), // send classfiles to a different directory + ) + +lazy val b = project.in(file("b")) + .settings( + Compile / unmanagedClasspath := Seq(Attributed.blank((ThisBuild / baseDirectory).value / "a-result-types-java-tasty.jar")), + scalacOptions += "-Ycheck:all", + ) diff --git a/sbt-test/pipelining/Yjava-tasty-result-types/project/DottyInjectedPlugin.scala b/sbt-test/pipelining/Yjava-tasty-result-types/project/DottyInjectedPlugin.scala new file mode 100644 index 000000000000..69f15d168bfc --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-result-types/project/DottyInjectedPlugin.scala @@ -0,0 +1,12 @@ +import sbt._ +import Keys._ + +object DottyInjectedPlugin extends AutoPlugin { + override def requires = plugins.JvmPlugin + override def trigger = allRequirements + + override val projectSettings = Seq( + scalaVersion := sys.props("plugin.scalaVersion"), + scalacOptions += "-source:3.0-migration" + ) +} diff --git a/sbt-test/pipelining/Yjava-tasty-result-types/test b/sbt-test/pipelining/Yjava-tasty-result-types/test new file mode 100644 index 000000000000..f654cb06fc16 --- /dev/null +++ b/sbt-test/pipelining/Yjava-tasty-result-types/test @@ -0,0 +1,3 @@ +> a/compile +# Test depending on a java static final result, and method result through TASTy +> b/compile diff --git a/tasty/src/dotty/tools/tasty/TastyFormat.scala b/tasty/src/dotty/tools/tasty/TastyFormat.scala index ebe94cc0a76c..6ceb82f011f4 100644 --- a/tasty/src/dotty/tools/tasty/TastyFormat.scala +++ b/tasty/src/dotty/tools/tasty/TastyFormat.scala @@ -89,6 +89,7 @@ Standard-Section: "ASTs" TopLevelStat* SELECTin Length possiblySigned_NameRef qual_Term owner_Type -- qual.name, referring to a symbol declared in owner that has the given signature (see note below) QUALTHIS typeIdent_Tree -- id.this, different from THIS in that it contains a qualifier ident with position. NEW clsType_Term -- new cls + ELIDED exprType_Type -- elided expression of the given type THROW throwableExpr_Term -- throw throwableExpr NAMEDARG paramName_NameRef arg_Term -- paramName = arg APPLY Length fn_Term arg_Term* -- fn(args) @@ -275,6 +276,8 @@ Standard Section: "Attributes" Attribute* EXPLICITNULLSattr CAPTURECHECKEDattr WITHPUREFUNSattr + JAVAattr + OUTLINEattr ``` **************************************************************************************/ @@ -525,6 +528,7 @@ object TastyFormat { final val SINGLETONtpt = 101 final val BOUNDED = 102 final val EXPLICITtpt = 103 + final val ELIDED = 104 // Cat. 4: tag Nat AST @@ -615,6 +619,8 @@ object TastyFormat { final val EXPLICITNULLSattr = 2 final val CAPTURECHECKEDattr = 3 final val WITHPUREFUNSattr = 4 + final val JAVAattr = 5 + final val OUTLINEattr = 6 /** Useful for debugging */ def isLegalTag(tag: Int): Boolean = @@ -622,7 +628,7 @@ object TastyFormat { firstNatTreeTag <= tag && tag <= RENAMED || firstASTTreeTag <= tag && tag <= BOUNDED || firstNatASTTreeTag <= tag && tag <= NAMEDARG || - firstLengthTreeTag <= tag && tag <= MATCHtpt || + firstLengthTreeTag <= tag && tag <= MATCHCASEtype || tag == HOLE def isParamTag(tag: Int): Boolean = tag == PARAM || tag == TYPEPARAM @@ -828,6 +834,7 @@ object TastyFormat { case PRIVATEqualified => "PRIVATEqualified" case PROTECTEDqualified => "PROTECTEDqualified" case EXPLICITtpt => "EXPLICITtpt" + case ELIDED => "ELIDED" case HOLE => "HOLE" } @@ -836,6 +843,8 @@ object TastyFormat { case EXPLICITNULLSattr => "EXPLICITNULLSattr" case CAPTURECHECKEDattr => "CAPTURECHECKEDattr" case WITHPUREFUNSattr => "WITHPUREFUNSattr" + case JAVAattr => "JAVAattr" + case OUTLINEattr => "OUTLINEattr" } /** @return If non-negative, the number of leading references (represented as nats) of a length/trees entry. From 9b12e4a3b1fa11e1203bde67cb67073ec6dc0b4e Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Mon, 27 Nov 2023 10:56:48 +0100 Subject: [PATCH 4/4] also test java tasty at runtime --- sbt-test/pipelining/Yjava-tasty-annotation/build.sbt | 2 +- sbt-test/pipelining/Yjava-tasty-enum/build.sbt | 6 +++++- sbt-test/pipelining/Yjava-tasty-enum/test | 2 +- .../Yjava-tasty-from-tasty/a/src/main/scala/a/A.java | 2 +- .../b/src/main/scala/b/B.scala | 6 +++++- sbt-test/pipelining/Yjava-tasty-from-tasty/build.sbt | 12 +++++++++--- sbt-test/pipelining/Yjava-tasty-from-tasty/test | 2 +- .../Yjava-tasty-generic/b/src/main/scala/b/B.scala | 5 ++++- sbt-test/pipelining/Yjava-tasty-generic/build.sbt | 6 +++++- sbt-test/pipelining/Yjava-tasty-generic/test | 2 +- .../b/src/main/scala/b/B.scala | 6 +++--- .../pipelining/Yjava-tasty-result-types/build.sbt | 6 +++++- sbt-test/pipelining/Yjava-tasty-result-types/test | 2 +- 13 files changed, 42 insertions(+), 17 deletions(-) diff --git a/sbt-test/pipelining/Yjava-tasty-annotation/build.sbt b/sbt-test/pipelining/Yjava-tasty-annotation/build.sbt index 28b9b0e2bee9..9299d94c060d 100644 --- a/sbt-test/pipelining/Yjava-tasty-annotation/build.sbt +++ b/sbt-test/pipelining/Yjava-tasty-annotation/build.sbt @@ -3,7 +3,7 @@ lazy val a = project.in(file("a")) scalacOptions += "-Yjava-tasty", // enable pickling of java signatures scalacOptions ++= Seq("-Yjava-tasty-output", ((ThisBuild / baseDirectory).value / "a-annotation-java-tasty.jar").toString), scalacOptions += "-Ycheck:all", - classDirectory := ((ThisBuild / baseDirectory).value / "a-annotation-classes"), // send classfiles to a different directory + Compile / classDirectory := ((ThisBuild / baseDirectory).value / "a-annotation-classes"), // send classfiles to a different directory ) lazy val b = project.in(file("b")) diff --git a/sbt-test/pipelining/Yjava-tasty-enum/build.sbt b/sbt-test/pipelining/Yjava-tasty-enum/build.sbt index 1c416c65896f..0c95d8318913 100644 --- a/sbt-test/pipelining/Yjava-tasty-enum/build.sbt +++ b/sbt-test/pipelining/Yjava-tasty-enum/build.sbt @@ -4,7 +4,7 @@ lazy val a = project.in(file("a")) scalacOptions += "-Yjava-tasty", // enable pickling of java signatures scalacOptions ++= Seq("-Yjava-tasty-output", ((ThisBuild / baseDirectory).value / "a-enum-java-tasty.jar").toString), scalacOptions += "-Ycheck:all", - classDirectory := ((ThisBuild / baseDirectory).value / "a-enum-classes"), // send classfiles to a different directory + Compile / classDirectory := ((ThisBuild / baseDirectory).value / "a-enum-classes"), // send classfiles to a different directory ) @@ -13,3 +13,7 @@ lazy val b = project.in(file("b")) Compile / unmanagedClasspath := Seq(Attributed.blank((ThisBuild / baseDirectory).value / "a-enum-java-tasty.jar")), scalacOptions += "-Ycheck:all", ) + .settings( + fork := true, // we have to fork the JVM if we actually want to run the code with correct failure semantics + Runtime / unmanagedClasspath += Attributed.blank((ThisBuild / baseDirectory).value / "a-enum-classes"), // make sure the java classes are visible at runtime + ) diff --git a/sbt-test/pipelining/Yjava-tasty-enum/test b/sbt-test/pipelining/Yjava-tasty-enum/test index 68e3c170d3b5..fe04b1a7c7ea 100644 --- a/sbt-test/pipelining/Yjava-tasty-enum/test +++ b/sbt-test/pipelining/Yjava-tasty-enum/test @@ -1,3 +1,3 @@ > a/compile # test depending on a java compiled enum through TASTy -> b/compile +> b/run diff --git a/sbt-test/pipelining/Yjava-tasty-from-tasty/a/src/main/scala/a/A.java b/sbt-test/pipelining/Yjava-tasty-from-tasty/a/src/main/scala/a/A.java index b8a278edc32f..381da612df90 100644 --- a/sbt-test/pipelining/Yjava-tasty-from-tasty/a/src/main/scala/a/A.java +++ b/sbt-test/pipelining/Yjava-tasty-from-tasty/a/src/main/scala/a/A.java @@ -1,5 +1,5 @@ package a; public class A { - public static final String VALUE = "A"; + public String VALUE = "A"; } diff --git a/sbt-test/pipelining/Yjava-tasty-from-tasty/b/src/main/scala/b/B.scala b/sbt-test/pipelining/Yjava-tasty-from-tasty/b/src/main/scala/b/B.scala index 884bf1a927ff..43a45ae53ce2 100644 --- a/sbt-test/pipelining/Yjava-tasty-from-tasty/b/src/main/scala/b/B.scala +++ b/sbt-test/pipelining/Yjava-tasty-from-tasty/b/src/main/scala/b/B.scala @@ -1,5 +1,9 @@ package b object B { - val A: "A" = a.A.VALUE + val A_VALUE = (new a.A).VALUE + + @main def test = { + assert(A_VALUE == "A", s"actually was $A_VALUE") + } } diff --git a/sbt-test/pipelining/Yjava-tasty-from-tasty/build.sbt b/sbt-test/pipelining/Yjava-tasty-from-tasty/build.sbt index dc4950ec8379..570e72a40c1b 100644 --- a/sbt-test/pipelining/Yjava-tasty-from-tasty/build.sbt +++ b/sbt-test/pipelining/Yjava-tasty-from-tasty/build.sbt @@ -5,7 +5,7 @@ lazy val a = project.in(file("a")) scalacOptions += "-Yjava-tasty", // enable pickling of java signatures scalacOptions ++= Seq("-Yjava-tasty-output", ((ThisBuild / baseDirectory).value / "a-pre-java-tasty.jar").toString), scalacOptions += "-Ycheck:all", - classDirectory := ((ThisBuild / baseDirectory).value / "a-pre-classes"), // send classfiles to a different directory + Compile / classDirectory := ((ThisBuild / baseDirectory).value / "a-pre-classes"), // send classfiles to a different directory ) // recompile `a` with `-from-tasty` flag to test idempotent read/write java signatures. @@ -19,11 +19,17 @@ lazy val a_from_tasty = project.in(file("a_from_tasty")) scalacOptions += "-Yallow-outline-from-tasty", // allow outline signatures to be read with -from-tasty scalacOptions ++= Seq("-Yjava-tasty-output", ((ThisBuild / baseDirectory).value / "a_from_tasty-java-tasty.jar").toString), scalacOptions += "-Ycheck:all", - classDirectory := ((ThisBuild / baseDirectory).value / "a_from_tasty-classes"), // send classfiles to a different directory + Compile / classDirectory := ((ThisBuild / baseDirectory).value / "a_from_tasty-classes"), // send classfiles to a different directory ) lazy val b = project.in(file("b")) .settings( - Compile / unmanagedClasspath := Seq(Attributed.blank((ThisBuild / baseDirectory).value / "a_from_tasty-java-tasty.jar")), scalacOptions += "-Ycheck:all", + Compile / unmanagedClasspath := Seq(Attributed.blank((ThisBuild / baseDirectory).value / "a_from_tasty-java-tasty.jar")), + ) + .settings( + // we have to fork the JVM if we actually want to run the code with correct failure semantics + fork := true, + // make sure the java classes are visible at runtime + Runtime / unmanagedClasspath += Attributed.blank((ThisBuild / baseDirectory).value / "a-pre-classes"), ) diff --git a/sbt-test/pipelining/Yjava-tasty-from-tasty/test b/sbt-test/pipelining/Yjava-tasty-from-tasty/test index af7eced8e846..5c08ed4c4458 100644 --- a/sbt-test/pipelining/Yjava-tasty-from-tasty/test +++ b/sbt-test/pipelining/Yjava-tasty-from-tasty/test @@ -2,4 +2,4 @@ # test reading java tasty with -from-tasty > a_from_tasty/compile # test java tasty is still written even with -from-tasty -> b/compile +> b/run diff --git a/sbt-test/pipelining/Yjava-tasty-generic/b/src/main/scala/b/B.scala b/sbt-test/pipelining/Yjava-tasty-generic/b/src/main/scala/b/B.scala index dcb4935860df..f132e012a5fc 100644 --- a/sbt-test/pipelining/Yjava-tasty-generic/b/src/main/scala/b/B.scala +++ b/sbt-test/pipelining/Yjava-tasty-generic/b/src/main/scala/b/B.scala @@ -7,6 +7,9 @@ class B[T] { } object B { - val derived: Int = (new B[Int]).inner.value + @main def test = { + val derived: Int = (new B[Int]).inner.value + assert(derived == 23, s"actually was $derived") + } } diff --git a/sbt-test/pipelining/Yjava-tasty-generic/build.sbt b/sbt-test/pipelining/Yjava-tasty-generic/build.sbt index aa5d3099e979..0c8a5c55fe7e 100644 --- a/sbt-test/pipelining/Yjava-tasty-generic/build.sbt +++ b/sbt-test/pipelining/Yjava-tasty-generic/build.sbt @@ -3,7 +3,7 @@ lazy val a = project.in(file("a")) scalacOptions += "-Yjava-tasty", // enable pickling of java signatures scalacOptions ++= Seq("-Yjava-tasty-output", ((ThisBuild / baseDirectory).value / "a-generic-java-tasty.jar").toString), scalacOptions += "-Ycheck:all", - classDirectory := ((ThisBuild / baseDirectory).value / "a-generic-classes"), // send classfiles to a different directory + Compile / classDirectory := ((ThisBuild / baseDirectory).value / "a-generic-classes"), // send classfiles to a different directory ) lazy val b = project.in(file("b")) @@ -11,3 +11,7 @@ lazy val b = project.in(file("b")) Compile / unmanagedClasspath := Seq(Attributed.blank((ThisBuild / baseDirectory).value / "a-generic-java-tasty.jar")), scalacOptions += "-Ycheck:all", ) + .settings( + fork := true, // we have to fork the JVM if we actually want to run the code with correct failure semantics + Runtime / unmanagedClasspath += Attributed.blank((ThisBuild / baseDirectory).value / "a-generic-classes"), // make sure the java classes are visible at runtime + ) diff --git a/sbt-test/pipelining/Yjava-tasty-generic/test b/sbt-test/pipelining/Yjava-tasty-generic/test index 5abac4b5eae7..cbe3e14572a8 100644 --- a/sbt-test/pipelining/Yjava-tasty-generic/test +++ b/sbt-test/pipelining/Yjava-tasty-generic/test @@ -1,3 +1,3 @@ > a/compile # Test depending on a java generic class through TASTy -> b/compile +> b/run diff --git a/sbt-test/pipelining/Yjava-tasty-result-types/b/src/main/scala/b/B.scala b/sbt-test/pipelining/Yjava-tasty-result-types/b/src/main/scala/b/B.scala index b67840f2f852..32d001075d40 100644 --- a/sbt-test/pipelining/Yjava-tasty-result-types/b/src/main/scala/b/B.scala +++ b/sbt-test/pipelining/Yjava-tasty-result-types/b/src/main/scala/b/B.scala @@ -9,9 +9,9 @@ object B { val a_true: String = (new A()).add(true) @main def test = { - assert(finalResult == "A") - assert(a_B == "AB") - assert(a_true == "Atrue") + assert(finalResult == "A", s"actually was $finalResult") + assert(a_B == "AB", s"actually was $a_B") + assert(a_true == "Atrue", s"actually was $a_true") } } diff --git a/sbt-test/pipelining/Yjava-tasty-result-types/build.sbt b/sbt-test/pipelining/Yjava-tasty-result-types/build.sbt index f9cf8082c731..8f9e782f8810 100644 --- a/sbt-test/pipelining/Yjava-tasty-result-types/build.sbt +++ b/sbt-test/pipelining/Yjava-tasty-result-types/build.sbt @@ -3,7 +3,7 @@ lazy val a = project.in(file("a")) scalacOptions += "-Yjava-tasty", // enable pickling of java signatures scalacOptions ++= Seq("-Yjava-tasty-output", ((ThisBuild / baseDirectory).value / "a-result-types-java-tasty.jar").toString), scalacOptions += "-Ycheck:all", - classDirectory := ((ThisBuild / baseDirectory).value / "a-result-types-classes"), // send classfiles to a different directory + Compile / classDirectory := ((ThisBuild / baseDirectory).value / "a-result-types-classes"), // send classfiles to a different directory ) lazy val b = project.in(file("b")) @@ -11,3 +11,7 @@ lazy val b = project.in(file("b")) Compile / unmanagedClasspath := Seq(Attributed.blank((ThisBuild / baseDirectory).value / "a-result-types-java-tasty.jar")), scalacOptions += "-Ycheck:all", ) + .settings( + fork := true, // we have to fork the JVM if we actually want to run the code with correct failure semantics + Runtime / unmanagedClasspath += Attributed.blank((ThisBuild / baseDirectory).value / "a-result-types-classes"), // make sure the java classes are visible at runtime + ) diff --git a/sbt-test/pipelining/Yjava-tasty-result-types/test b/sbt-test/pipelining/Yjava-tasty-result-types/test index f654cb06fc16..c1cbbb1f2fe5 100644 --- a/sbt-test/pipelining/Yjava-tasty-result-types/test +++ b/sbt-test/pipelining/Yjava-tasty-result-types/test @@ -1,3 +1,3 @@ > a/compile # Test depending on a java static final result, and method result through TASTy -> b/compile +> b/run