diff --git a/.github/workflows/enso4igv.yml b/.github/workflows/enso4igv.yml index 0437224a1360..1b21a524a476 100644 --- a/.github/workflows/enso4igv.yml +++ b/.github/workflows/enso4igv.yml @@ -128,6 +128,12 @@ jobs: java-version: "21" distribution: "graalvm-community" + - name: Clean sbt + run: sbt clean + + - name: WIP Print sbt config + run: sbt 'print runtime-parser/Compile/doc/sources' + - name: Publish Enso Libraries to Local Maven Repository run: sbt publishM2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b5e023d18db..19617904c38f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,11 +18,13 @@ previously allowed to use spaces in the argument definition without parentheses. [This is now a syntax error.][11856] - Symetric, transitive and reflexive [equality for intersection types][11897] +- [IR definitions are generated by an annotation processor][11770] [11777]: https://github.com/enso-org/enso/pull/11777 [11600]: https://github.com/enso-org/enso/pull/11600 [11856]: https://github.com/enso-org/enso/pull/11856 [11897]: https://github.com/enso-org/enso/pull/11897 +[11770]: https://github.com/enso-org/enso/pull/11770 # Next Release diff --git a/build.sbt b/build.sbt index a7491b8881c3..10fe915ffba9 100644 --- a/build.sbt +++ b/build.sbt @@ -13,6 +13,8 @@ import src.main.scala.licenses.{ SBTDistributionComponent } +import scala.collection.mutable.ArrayBuffer + // This import is unnecessary, but bit adds a proper code completion features // to IntelliJ. import JPMSPlugin.autoImport._ @@ -353,6 +355,9 @@ lazy val enso = (project in file(".")) `runtime-compiler`, `runtime-integration-tests`, `runtime-parser`, + `runtime-parser-dsl`, + `runtime-parser-processor`, + `runtime-parser-processor-tests`, `runtime-language-arrow`, `runtime-language-epb`, `runtime-instrument-common`, @@ -2574,6 +2579,13 @@ lazy val mixedJavaScalaProjectSetting: SettingsDefinition = Seq( excludeFilter := excludeFilter.value || "module-info.java" ) +/** Ensure that javac compiler generates parameter names for methods, so that these + * Java methods can be called with named parameters from Scala. + */ +lazy val javaMethodParametersSetting: SettingsDefinition = Seq( + javacOptions += "-parameters" +) + def customFrgaalJavaCompilerSettings(targetJdk: String) = { // There might be slightly different Frgaal compiler configuration for // both Compile and Test configurations @@ -3198,15 +3210,35 @@ lazy val `runtime-benchmarks` = lazy val `runtime-parser` = (project in file("engine/runtime-parser")) .enablePlugins(JPMSPlugin) + .enablePlugins(PackageListPlugin) .settings( scalaModuleDependencySetting, mixedJavaScalaProjectSetting, + javaMethodParametersSetting, version := mavenUploadVersion, javadocSettings, publish / skip := false, crossPaths := false, frgaalJavaCompilerSetting, annotationProcSetting, + // Explicitly list generated sources so that `scaladoc` knows where to find them. + // This is necessary because some Java classes extend from the generated classes. + // By default, `scaladoc` is unaware of the generated Java sources and would fail. + Compile / doc / sources ++= { + val managedSrcDir = (Compile / sourceManaged).value + val pkgs = (Compile / packages).value + val generatedJavaSrcs = new ArrayBuffer[File]() + pkgs.foreach { pkg => + val pathPart = pkg.replace(".", File.separator) + val dir = managedSrcDir.toPath.resolve(pathPart) + IO.listFiles(dir.toFile).foreach { file => + if (file.getName.endsWith(".java")) { + generatedJavaSrcs += file + } + } + } + generatedJavaSrcs.toList + }, commands += WithDebugCommand.withDebug, fork := true, libraryDependencies ++= Seq( @@ -3218,14 +3250,76 @@ lazy val `runtime-parser` = Compile / moduleDependencies ++= Seq( "org.netbeans.api" % "org-openide-util-lookup" % netbeansApiVersion ), + // Java compiler is not able to correctly find all the annotation processors, because + // one of them is on module-path. To overcome this, we explicitly list all of them here. + Compile / javacOptions ++= { + val processorClasses = Seq( + "org.enso.runtime.parser.processor.IRProcessor", + "org.enso.persist.impl.PersistableProcessor", + "org.netbeans.modules.openide.util.ServiceProviderProcessor", + "org.netbeans.modules.openide.util.NamedServiceProcessor" + ).mkString(",") + Seq( + "-processor", + processorClasses + ) + }, Compile / internalModuleDependencies := Seq( (`syntax-rust-definition` / Compile / exportedModule).value, - (`persistance` / Compile / exportedModule).value + (`persistance` / Compile / exportedModule).value, + (`runtime-parser-dsl` / Compile / exportedModule).value, + (`runtime-parser-processor` / Compile / exportedModule).value ) ) .dependsOn(`syntax-rust-definition`) .dependsOn(`persistance`) .dependsOn(`persistance-dsl` % "provided") + .dependsOn(`runtime-parser-dsl`) + .dependsOn(`runtime-parser-processor`) + +lazy val `runtime-parser-dsl` = + (project in file("engine/runtime-parser-dsl")) + .enablePlugins(JPMSPlugin) + .settings( + frgaalJavaCompilerSetting, + javaMethodParametersSetting + ) + +lazy val `runtime-parser-processor-tests` = + (project in file("engine/runtime-parser-processor-tests")) + .settings( + inConfig(Compile)(truffleRunOptionsSettings), + frgaalJavaCompilerSetting, + javaMethodParametersSetting, + commands += WithDebugCommand.withDebug, + annotationProcSetting, + Compile / javacOptions ++= Seq( + "-processor", + "org.enso.runtime.parser.processor.IRProcessor" + ), + Test / fork := true, + libraryDependencies ++= Seq( + "junit" % "junit" % junitVersion % Test, + "com.github.sbt" % "junit-interface" % junitIfVersion % Test, + "org.hamcrest" % "hamcrest-all" % hamcrestVersion % Test, + "com.google.testing.compile" % "compile-testing" % "0.21.0" % Test, + "org.scalatest" %% "scalatest" % scalatestVersion % Test + ) + ) + .dependsOn(`runtime-parser-processor`) + .dependsOn(`runtime-parser`) + +lazy val `runtime-parser-processor` = + (project in file("engine/runtime-parser-processor")) + .enablePlugins(JPMSPlugin) + .settings( + frgaalJavaCompilerSetting, + javaMethodParametersSetting, + Compile / internalModuleDependencies := Seq( + (`runtime-parser-dsl` / Compile / exportedModule).value + ) + ) + .dependsOn(`runtime-parser-dsl`) lazy val `runtime-compiler` = (project in file("engine/runtime-compiler")) diff --git a/engine/runtime-compiler/src/main/scala/org/enso/compiler/pass/optimise/LambdaConsolidate.scala b/engine/runtime-compiler/src/main/scala/org/enso/compiler/pass/optimise/LambdaConsolidate.scala index 6f2ab1e20910..6b32d472bc16 100644 --- a/engine/runtime-compiler/src/main/scala/org/enso/compiler/pass/optimise/LambdaConsolidate.scala +++ b/engine/runtime-compiler/src/main/scala/org/enso/compiler/pass/optimise/LambdaConsolidate.scala @@ -227,7 +227,7 @@ case object LambdaConsolidate extends IRPass { } val shadower: IR = - mShadower.getOrElse(Empty(spec.identifiedLocation)) + mShadower.getOrElse(new Empty(spec.identifiedLocation)) spec.getDiagnostics.add( warnings.Shadowed diff --git a/engine/runtime-compiler/src/main/scala/org/enso/compiler/pass/resolve/SuspendedArguments.scala b/engine/runtime-compiler/src/main/scala/org/enso/compiler/pass/resolve/SuspendedArguments.scala index 26629b1c9478..e5f50d37b0f4 100644 --- a/engine/runtime-compiler/src/main/scala/org/enso/compiler/pass/resolve/SuspendedArguments.scala +++ b/engine/runtime-compiler/src/main/scala/org/enso/compiler/pass/resolve/SuspendedArguments.scala @@ -306,7 +306,7 @@ case object SuspendedArguments extends IRPass { } else if (args.length > signatureSegments.length) { val additionalSegments = signatureSegments ::: List.fill( args.length - signatureSegments.length - )(Empty(identifiedLocation = null)) + )(new Empty(null)) args.zip(additionalSegments) } else { diff --git a/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/core/ir/DiagnosticStorageTest.scala b/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/core/ir/DiagnosticStorageTest.scala index a119cfe64321..f2454dcf43ef 100644 --- a/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/core/ir/DiagnosticStorageTest.scala +++ b/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/core/ir/DiagnosticStorageTest.scala @@ -16,7 +16,7 @@ class DiagnosticStorageTest extends CompilerTest { def mkDiagnostic(name: String): Diagnostic = { warnings.Shadowed.FunctionParam( name, - Empty(identifiedLocation = null), + new Empty(null), identifiedLocation = null ) } diff --git a/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/pass/desugar/OperatorToFunctionTest.scala b/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/pass/desugar/OperatorToFunctionTest.scala index 46e209ab4650..48828c6e3ec6 100644 --- a/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/pass/desugar/OperatorToFunctionTest.scala +++ b/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/pass/desugar/OperatorToFunctionTest.scala @@ -84,9 +84,9 @@ class OperatorToFunctionTest extends MiniPassTest { // === The Tests ============================================================ val opName = Name.Literal("=:=", isMethod = true, null) - val left = Empty(null) - val right = Empty(null) - val rightArg = new CallArgument.Specified(None, Empty(null), false, null) + val left = new Empty(null) + val right = new Empty(null) + val rightArg = new CallArgument.Specified(None, new Empty(null), false, null) val (operator, operatorFn) = genOprAndFn(opName, left, right) @@ -96,11 +96,11 @@ class OperatorToFunctionTest extends MiniPassTest { "Operators" should { val opName = Name.Literal("=:=", isMethod = true, identifiedLocation = null) - val left = Empty(identifiedLocation = null) - val right = Empty(identifiedLocation = null) + val left = new Empty(null) + val right = new Empty(null) val rightArg = new CallArgument.Specified( None, - Empty(identifiedLocation = null), + new Empty(null), false, identifiedLocation = null ) diff --git a/engine/runtime-parser-dsl/src/main/java/module-info.java b/engine/runtime-parser-dsl/src/main/java/module-info.java new file mode 100644 index 000000000000..c1e759c70246 --- /dev/null +++ b/engine/runtime-parser-dsl/src/main/java/module-info.java @@ -0,0 +1,3 @@ +module org.enso.runtime.parser.dsl { + exports org.enso.runtime.parser.dsl; +} diff --git a/engine/runtime-parser-dsl/src/main/java/org/enso/runtime/parser/dsl/GenerateFields.java b/engine/runtime-parser-dsl/src/main/java/org/enso/runtime/parser/dsl/GenerateFields.java new file mode 100644 index 000000000000..05ff1f020be9 --- /dev/null +++ b/engine/runtime-parser-dsl/src/main/java/org/enso/runtime/parser/dsl/GenerateFields.java @@ -0,0 +1,51 @@ +package org.enso.runtime.parser.dsl; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Parameters of the constructor annotated with this annotation will be scanned by the IR processor + * and fieldswill be generated for them. There can be only a single constructor with this + * annotation in a class. The enclosing class must be annotated with {@link GenerateIR}. + * + *

Fields

+ * + * The generated class will contain 4 meta fields that are required to be present inside + * every IR element: + * + * + * + * Apart from these meta fields, the generated class will also contain user-defined + * fields. User-defined fields are inferred from all the parameters of the constructor annotated + * with {@link GenerateFields}. The parameter of the constructor can be one of the following: + * + * + * + *

A user-defined field generated out of constructor parameter annotated with {@link IRChild} is + * a child element of this IR element. That means that it will be included in generated + * implementation of IR methods that iterate over the IR tree. For example {@code mapExpressions} or + * {@code children}. + * + *

A user-defined field generated out of constructor parameter annotated with {@link IRField} + * will be a field with generated getters. Such field however, will not be part of the IR tree + * traversal methods. + * + *

For a constructor parameter of a meta type, there will be no user-defined field generated, as + * the meta fields are always generated. + * + *

Other types of constructor parameters are forbidden. + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.CONSTRUCTOR) +public @interface GenerateFields {} diff --git a/engine/runtime-parser-dsl/src/main/java/org/enso/runtime/parser/dsl/GenerateIR.java b/engine/runtime-parser-dsl/src/main/java/org/enso/runtime/parser/dsl/GenerateIR.java new file mode 100644 index 000000000000..64f68945066c --- /dev/null +++ b/engine/runtime-parser-dsl/src/main/java/org/enso/runtime/parser/dsl/GenerateIR.java @@ -0,0 +1,32 @@ +package org.enso.runtime.parser.dsl; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A class annotated with this annotation will be processed by the IR processor. The processor will + * generate a super class from the {@code extends} clause of the annotated class. If the annotated + * class does not have {@code extends} clause, an error is generated. Moreover, if the class in the + * {@code extends} clause already exists, an error is generated. + * + *

The generated class will have the same package as the annotated class. Majority of the methods + * in the generated class will be either private or package-private, so that they are not accessible + * from the outside. + * + *

The class can be enclosed (nested inside) an interface. + * + *

The class must contain a single constructor annotated with {@link GenerateFields}. + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface GenerateIR { + + /** + * Interfaces that the generated superclass will implement. The list of the interfaces will simply + * be put inside the {@code implements} clause of the generated class. All the generated classes + * implement {@code org.enso.compiler.core.IR} by default. + */ + Class[] interfaces() default {}; +} diff --git a/engine/runtime-parser-dsl/src/main/java/org/enso/runtime/parser/dsl/IRChild.java b/engine/runtime-parser-dsl/src/main/java/org/enso/runtime/parser/dsl/IRChild.java new file mode 100644 index 000000000000..46195a57c1ab --- /dev/null +++ b/engine/runtime-parser-dsl/src/main/java/org/enso/runtime/parser/dsl/IRChild.java @@ -0,0 +1,19 @@ +package org.enso.runtime.parser.dsl; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Constructor parameter annotated with this annotation will be represented as a child field in the + * generated super class. Children of IR elements form a tree. A child will be part of the methods + * traversing the tree, like {@code mapExpression} and {@code children}. The parameter type must be + * a subtype of {@code org.enso.compiler.ir.IR}. + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.PARAMETER) +public @interface IRChild { + /** If true, the child will always be non-null. Otherwise, it can be null. */ + boolean required() default true; +} diff --git a/engine/runtime-parser-dsl/src/main/java/org/enso/runtime/parser/dsl/IRField.java b/engine/runtime-parser-dsl/src/main/java/org/enso/runtime/parser/dsl/IRField.java new file mode 100644 index 000000000000..c850b84d1746 --- /dev/null +++ b/engine/runtime-parser-dsl/src/main/java/org/enso/runtime/parser/dsl/IRField.java @@ -0,0 +1,19 @@ +package org.enso.runtime.parser.dsl; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A constructor parameter annotated with this annotation will have a corresponding user-defined + * field generated in the super class (See {@link GenerateFields} for docs about fields). + * + *

There is no restriction on the type of the parameter. + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.PARAMETER) +public @interface IRField { + /** If true, the field will always be non-null. Otherwise, it can be null. */ + boolean required() default true; +} diff --git a/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/core/JCallArgument.java b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/core/JCallArgument.java new file mode 100644 index 000000000000..12a8596bfc15 --- /dev/null +++ b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/core/JCallArgument.java @@ -0,0 +1,42 @@ +package org.enso.runtime.parser.processor.test.gen.ir.core; + +import java.util.function.Function; +import org.enso.compiler.core.IR; +import org.enso.compiler.core.ir.Expression; +import org.enso.compiler.core.ir.Name; +import org.enso.runtime.parser.dsl.GenerateFields; +import org.enso.runtime.parser.dsl.GenerateIR; +import org.enso.runtime.parser.dsl.IRChild; +import org.enso.runtime.parser.dsl.IRField; +import scala.Option; + +/** Call-site arguments in Enso. */ +public interface JCallArgument extends IR { + /** The name of the argument, if present. */ + Option name(); + + /** The expression of the argument, if present. */ + Expression value(); + + /** Flag indicating that the argument was generated by compiler. */ + boolean isSynthetic(); + + @Override + JCallArgument mapExpressions(Function fn); + + @Override + JCallArgument duplicate( + boolean keepLocations, + boolean keepMetadata, + boolean keepDiagnostics, + boolean keepIdentifiers); + + @GenerateIR(interfaces = {JCallArgument.class}) + final class JSpecified extends JSpecifiedGen { + @GenerateFields + public JSpecified( + @IRField boolean isSynthetic, @IRChild Option name, @IRChild Expression value) { + super(isSynthetic, name, value); + } + } +} diff --git a/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/core/JExpression.java b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/core/JExpression.java new file mode 100644 index 000000000000..92116fbfd0d3 --- /dev/null +++ b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/core/JExpression.java @@ -0,0 +1,42 @@ +package org.enso.runtime.parser.processor.test.gen.ir.core; + +import java.util.function.Function; +import org.enso.compiler.core.IR; +import org.enso.compiler.core.ir.Expression; +import org.enso.compiler.core.ir.Name; +import org.enso.runtime.parser.dsl.GenerateFields; +import org.enso.runtime.parser.dsl.GenerateIR; +import org.enso.runtime.parser.dsl.IRChild; +import org.enso.runtime.parser.dsl.IRField; +import scala.collection.immutable.List; + +public interface JExpression extends IR { + @Override + JExpression mapExpressions(Function fn); + + @Override + JExpression duplicate( + boolean keepLocations, + boolean keepMetadata, + boolean keepDiagnostics, + boolean keepIdentifiers); + + @GenerateIR(interfaces = {JExpression.class}) + final class JBlock extends JBlockGen { + @GenerateFields + public JBlock( + @IRChild List expressions, + @IRChild JExpression returnValue, + @IRField boolean suspended) { + super(expressions, returnValue, suspended); + } + } + + @GenerateIR(interfaces = {JExpression.class}) + final class JBinding extends JBindingGen { + @GenerateFields + public JBinding(@IRChild Name name, @IRChild JExpression expression) { + super(name, expression); + } + } +} diff --git a/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/core/package-info.java b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/core/package-info.java new file mode 100644 index 000000000000..654c976fd82e --- /dev/null +++ b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/core/package-info.java @@ -0,0 +1,9 @@ +/** + * Contains hierarchy of interfaces that should correspond to the previous {@link + * org.enso.compiler.core.IR} element hierarchy. All the classes inside this package have {@code J} + * prefix. So for example {@code JCallArgument} correspond to {@code CallArgument}. + * + *

The motivation to put these classes here is to test the generation of {@link + * org.enso.runtime.parser.processor.IRProcessor}. + */ +package org.enso.runtime.parser.processor.test.gen.ir.core; diff --git a/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/package-info.java b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/package-info.java new file mode 100644 index 000000000000..d571af3ee3cc --- /dev/null +++ b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/package-info.java @@ -0,0 +1,5 @@ +/** + * Contains interfaces with parser-dsl annotations. There will be generated classes for these + * interfaces and they are tested. All these interfaces are only for testing. + */ +package org.enso.runtime.parser.processor.test.gen.ir; diff --git a/engine/runtime-parser-processor-tests/src/test/java/org/enso/runtime/parser/processor/test/TestIRProcessorInline.java b/engine/runtime-parser-processor-tests/src/test/java/org/enso/runtime/parser/processor/test/TestIRProcessorInline.java new file mode 100644 index 000000000000..e0866a4538e2 --- /dev/null +++ b/engine/runtime-parser-processor-tests/src/test/java/org/enso/runtime/parser/processor/test/TestIRProcessorInline.java @@ -0,0 +1,640 @@ +package org.enso.runtime.parser.processor.test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.CompilationSubject; +import com.google.testing.compile.Compiler; +import com.google.testing.compile.JavaFileObjects; +import java.io.IOException; +import org.enso.runtime.parser.processor.IRProcessor; +import org.junit.Test; + +/** + * Basic tests of {@link IRProcessor} that compiles snippets of annotated code, and checks the + * generated classes. The compiler (along with the processor) is invoked in the unit tests. + */ +public class TestIRProcessorInline { + /** + * Compiles the code given in {@code src} with {@link IRProcessor} and returns the contents of the + * generated java source file. + * + * @param name FQN of the Java source file + * @param src + * @return + */ + private static String generatedClass(String name, String src) { + var srcObject = JavaFileObjects.forSourceString(name, src); + var compiler = Compiler.javac().withProcessors(new IRProcessor()); + var compilation = compiler.compile(srcObject); + CompilationSubject.assertThat(compilation).succeeded(); + assertThat("Generated just one source", compilation.generatedSourceFiles().size(), is(1)); + var generatedSrc = compilation.generatedSourceFiles().get(0); + try { + return generatedSrc.getCharContent(false).toString(); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + private static void expectCompilationFailure(String src) { + var srcObject = JavaFileObjects.forSourceString("TestHello", src); + var compiler = Compiler.javac().withProcessors(new IRProcessor()); + var compilation = compiler.compile(srcObject); + CompilationSubject.assertThat(compilation).failed(); + } + + private static Compilation compile(String name, String src) { + var srcObject = JavaFileObjects.forSourceString(name, src); + var compiler = Compiler.javac().withProcessors(new IRProcessor()); + var compilation = compiler.compile(srcObject); + return compilation; + } + + @Test + public void simpleIRNodeWithoutFields_CompilationSucceeds() { + var src = + JavaFileObjects.forSourceString( + "JName", + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + + @GenerateIR + public final class JName extends JNameGen { + @GenerateFields + public JName() {} + } + """); + var compiler = Compiler.javac().withProcessors(new IRProcessor()); + var compilation = compiler.compile(src); + CompilationSubject.assertThat(compilation).succeeded(); + } + + @Test + public void onlyFinalClassCanBeAnnotated() { + var src = + JavaFileObjects.forSourceString( + "JName", + """ + import org.enso.runtime.parser.dsl.GenerateIR; + @GenerateIR + public class JName {} + """); + var compiler = Compiler.javac().withProcessors(new IRProcessor()); + var compilation = compiler.compile(src); + CompilationSubject.assertThat(compilation).failed(); + CompilationSubject.assertThat(compilation).hadErrorCount(1); + CompilationSubject.assertThat(compilation).hadErrorContaining("final"); + } + + @Test + public void annotatedClass_MustHaveAnnotatedConstructor() { + var src = + JavaFileObjects.forSourceString( + "JName", + """ + import org.enso.runtime.parser.dsl.GenerateIR; + @GenerateIR + public final class JName {} + """); + var compiler = Compiler.javac().withProcessors(new IRProcessor()); + var compilation = compiler.compile(src); + CompilationSubject.assertThat(compilation).failed(); + CompilationSubject.assertThat(compilation) + .hadErrorContaining("must have exactly one constructor annotated with @GenerateFields"); + } + + @Test + public void annotatedClass_MustExtendGeneratedSuperclass() { + var src = + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + + @GenerateIR + public final class JName { + @GenerateFields + public JName() {} + } + """; + var compilation = compile("JName", src); + CompilationSubject.assertThat(compilation).failed(); + CompilationSubject.assertThat(compilation).hadErrorContaining("must have 'extends' clause"); + } + + @Test + public void annotatedClass_InterfacesToImplement_CanHaveMore() { + var src = + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + import org.enso.compiler.core.IR; + + interface MySuperIR { } + + @GenerateIR(interfaces = {MySuperIR.class, IR.class}) + public final class MyIR extends MyIRGen { + @GenerateFields + public MyIR() {} + } + """; + var generatedClass = generatedClass("MyIR", src); + assertThat(generatedClass, containsString("class MyIRGen implements IR, MySuperIR")); + } + + @Test + public void annotatedClass_InterfacesToImplement_DoNotHaveToExtendIR() { + var src = + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + + interface MySuperIR_1 { } + interface MySuperIR_2 { } + + @GenerateIR(interfaces = {MySuperIR_1.class, MySuperIR_2.class}) + public final class MyIR extends MyIRGen { + @GenerateFields + public MyIR() {} + } +"""; + var generatedClass = generatedClass("MyIR", src); + assertThat( + generatedClass, containsString("class MyIRGen implements IR, MySuperIR_1, MySuperIR_2")); + } + + @Test + public void simpleIRNodeWithUserDefinedFiled_CompilationSucceeds() { + var src = + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + import org.enso.runtime.parser.dsl.IRField; + + @GenerateIR + public final class JName extends JNameGen { + @GenerateFields + public JName(@IRField String name) { + super(name); + } + } + """; + var genClass = generatedClass("JName", src); + assertThat(genClass, containsString("class JNameGen")); + assertThat("Getter for 'name' generated", genClass, containsString("String name()")); + } + + @Test + public void generatedClassHasProtectedConstructor() { + var src = + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + + @GenerateIR + public final class JName extends JNameGen { + @GenerateFields + public JName() {} + } + """; + var genClass = generatedClass("JName", src); + assertThat(genClass, containsString("class JNameGen")); + assertThat( + "Generate class has protected constructor", genClass, containsString("protected JNameGen")); + } + + /** + * The default generated protected constructor has the same signature as the constructor in + * subtype annotated with {@link org.enso.runtime.parser.dsl.GenerateFields} + */ + @Test + public void generatedClassHasConstructorMatchingSubtype_Empty() { + var src = + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + + @GenerateIR + public final class JName extends JNameGen { + @GenerateFields + public JName() { + super(); + } + } + """; + var compilation = compile("JName", src); + CompilationSubject.assertThat(compilation).succeeded(); + } + + @Test + public void generatedClassHasConstructorMatchingSubtype_UserFields() { + var src = + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + import org.enso.runtime.parser.dsl.IRField; + + @GenerateIR + public final class JName extends JNameGen { + @GenerateFields + public JName(@IRField boolean suspended, @IRField String name) { + super(suspended, name); + } + } + """; + var compilation = compile("JName", src); + CompilationSubject.assertThat(compilation).succeeded(); + } + + @Test + public void generatedClassHasConstructorMatchingSubtype_MetaFields() { + var src = + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + import org.enso.compiler.core.ir.DiagnosticStorage; + import org.enso.compiler.core.ir.IdentifiedLocation; + + @GenerateIR + public final class JName extends JNameGen { + @GenerateFields + public JName(DiagnosticStorage diag, IdentifiedLocation loc) { + super(diag, loc); + } + } + """; + var compilation = compile("JName", src); + CompilationSubject.assertThat(compilation).succeeded(); + } + + @Test + public void generatedClassHasConstructorMatchingSubtype_UserFieldsAndMetaFields() { + var src = + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + import org.enso.runtime.parser.dsl.IRField; + import org.enso.compiler.core.ir.DiagnosticStorage; + import org.enso.compiler.core.ir.IdentifiedLocation; + + @GenerateIR + public final class JName extends JNameGen { + @GenerateFields + public JName(DiagnosticStorage diag, IdentifiedLocation loc, @IRField boolean suspended) { + super(diag, loc, suspended); + } + } + """; + var compilation = compile("JName", src); + CompilationSubject.assertThat(compilation).succeeded(); + } + + @Test + public void generatedClass_IsAbstract() { + var src = + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + + @GenerateIR + public final class JName extends JNameGen { + @GenerateFields + public JName() {} + } + """; + var genClass = generatedClass("JName", src); + assertThat(genClass, containsString("abstract class JNameGen")); + } + + @Test + public void generatedClass_CanHaveArbitraryName() { + var src = + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + + @GenerateIR + public final class JName extends MySuperGeneratedClass { + @GenerateFields + public JName() {} + } + """; + var genClass = generatedClass("JName", src); + assertThat(genClass, containsString("abstract class MySuperGeneratedClass")); + } + + /** + * Generated {@code duplicate} method returns the annotated class type, not any of its super + * types. + */ + @Test + public void generatedClass_DuplicateMethodHasSpecificReturnType() { + var src = + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + + @GenerateIR + public final class JName extends JNameGen { + @GenerateFields + public JName() {} + } + """; + var genClass = generatedClass("JName", src); + assertThat(genClass, containsString("JName duplicate(")); + } + + /** Parameterless {@code duplicate} method just delegates to the other duplicate method. */ + @Test + public void generatedClass_HasParameterlessDuplicateMethod() { + var src = + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + + @GenerateIR + public final class JName extends JNameGen { + @GenerateFields + public JName() {} + } + """; + var genClass = generatedClass("JName", src); + assertThat(genClass, containsString("JName duplicate()")); + } + + @Test + public void generatedMethod_setLocation_returnsSubClassType() { + var src = + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + + @GenerateIR + public final class JName extends JNameGen { + @GenerateFields + public JName() {} + } + """; + var genClass = generatedClass("JName", src); + assertThat(genClass, containsString("JName setLocation(")); + } + + @Test + public void generatedMethod_mapExpressions_returnsSubClassType() { + var src = + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + + @GenerateIR + public final class JName extends JNameGen { + @GenerateFields + public JName() {} + } + """; + var genClass = generatedClass("JName", src); + assertThat(genClass, containsString("JName mapExpressions(")); + } + + @Test + public void annotatedConstructor_MustNotHaveUnannotatedParameters() { + var src = + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + + @GenerateIR + public final class JName extends JNameGen { + @GenerateFields + public JName(int param) {} + } + """; + var compilation = compile("JName", src); + CompilationSubject.assertThat(compilation).failed(); + CompilationSubject.assertThat(compilation).hadErrorContaining("must be annotated"); + } + + @Test + public void annotatedConstructor_CanHaveMetaParameters() { + var src = + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + import org.enso.compiler.core.ir.MetadataStorage; + + @GenerateIR + public final class JName extends JNameGen { + @GenerateFields + public JName(MetadataStorage passData) { + super(passData); + } + } + """; + var compilation = compile("JName", src); + CompilationSubject.assertThat(compilation).succeeded(); + } + + @Test + public void simpleIRNodeWithChild() { + var genSrc = + generatedClass( + "MyIR", + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + import org.enso.runtime.parser.dsl.IRChild; + import org.enso.compiler.core.ir.Expression; + + @GenerateIR + public final class MyIR extends MyIRGen { + @GenerateFields + public MyIR(@IRChild Expression expression) { + super(expression); + } + } + """); + assertThat(genSrc, containsString("Expression expression()")); + } + + @Test + public void irNodeWithMultipleFields_PrimitiveField() { + var genSrc = + generatedClass( + "MyIR", + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + import org.enso.runtime.parser.dsl.IRChild; + import org.enso.runtime.parser.dsl.IRField; + + @GenerateIR + public final class MyIR extends MyIRGen { + @GenerateFields + public MyIR(@IRField boolean suspended) { + super(suspended); + } + } + """); + assertThat(genSrc, containsString("boolean suspended()")); + } + + @Test + public void irNodeWithInheritedField() { + var src = + generatedClass( + "MyIR", + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + import org.enso.runtime.parser.dsl.IRField; + import org.enso.compiler.core.IR; + + interface MySuperIR extends IR { + boolean suspended(); + } + + @GenerateIR(interfaces = {MySuperIR.class}) + public final class MyIR extends MyIRGen { + @GenerateFields + public MyIR(@IRField boolean suspended) { + super(suspended); + } + } + """); + assertThat(src, containsString("boolean suspended()")); + } + + @Test + public void irNodeWithInheritedField_Override() { + var src = + generatedClass( + "MyIR", + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + import org.enso.runtime.parser.dsl.IRField; + import org.enso.compiler.core.IR; + + interface MySuperIR extends IR { + boolean suspended(); + } + + @GenerateIR + public final class MyIR extends MyIRGen { + @GenerateFields + public MyIR(@IRField boolean suspended) { + super(suspended); + } + } + + """); + assertThat(src, containsString("boolean suspended()")); + } + + @Test + public void irNodeWithInheritedField_Transitive() { + var src = + generatedClass( + "MyIR", + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + import org.enso.runtime.parser.dsl.IRField; + import org.enso.compiler.core.IR; + + interface MySuperSuperIR extends IR { + boolean suspended(); + } + + interface MySuperIR extends MySuperSuperIR { + } + + @GenerateIR(interfaces = {MySuperIR.class}) + public final class MyIR extends MyIRGen { + @GenerateFields + public MyIR(@IRField boolean suspended) { + super(suspended); + } + } + """); + assertThat(src, containsString("boolean suspended()")); + } + + @Test + public void irNodeAsNestedClass() { + var src = + generatedClass( + "JName", + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + import org.enso.runtime.parser.dsl.IRField; + import org.enso.compiler.core.IR; + + public interface JName extends IR { + String name(); + + @GenerateIR(interfaces = {JName.class}) + public final class JBlank extends JBlankGen { + @GenerateFields + public JBlank(@IRField String name) { + super(name); + } + } + } + """); + assertThat(src, containsString("class JBlankGen implements IR, JName")); + assertThat(src, containsString("String name()")); + } + + @Test + public void fieldCanBeScalaList() { + var src = + generatedClass( + "JName", + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + import org.enso.runtime.parser.dsl.IRChild; + import org.enso.compiler.core.IR; + import scala.collection.immutable.List; + + @GenerateIR + public final class JName extends JNameGen { + @GenerateFields + public JName(@IRChild List expressions) { + super(expressions); + } + } + """); + assertThat(src, containsString("class JNameGen")); + assertThat(src, containsString("List expressions")); + } + + @Test + public void fieldCanBeScalaOption() { + var src = + generatedClass( + "JName", + """ + import org.enso.runtime.parser.dsl.GenerateIR; + import org.enso.runtime.parser.dsl.GenerateFields; + import org.enso.runtime.parser.dsl.IRChild; + import org.enso.compiler.core.IR; + import scala.Option; + + @GenerateIR + public final class JName extends JNameGen { + @GenerateFields + public JName(@IRChild Option expression) { + super(expression); + } + } + """); + assertThat(src, containsString("class JNameGen")); + assertThat("has getter method for expression", src, containsString("Option expression()")); + } +} diff --git a/engine/runtime-parser-processor-tests/src/test/scala/org/enso/runtime/parser/processor/test/GeneratedIRTest.scala b/engine/runtime-parser-processor-tests/src/test/scala/org/enso/runtime/parser/processor/test/GeneratedIRTest.scala new file mode 100644 index 000000000000..d545c9428412 --- /dev/null +++ b/engine/runtime-parser-processor-tests/src/test/scala/org/enso/runtime/parser/processor/test/GeneratedIRTest.scala @@ -0,0 +1,37 @@ +package org.enso.runtime.parser.processor.test + +import org.enso.compiler.core.ir.{Literal, MetadataStorage} +import org.enso.runtime.parser.processor.test.gen.ir.core.JCallArgument.JSpecified +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +/** Tests IR elements generated from package [[org.enso.runtime.parser.processor.test.gen.ir]]. + */ +class GeneratedIRTest extends AnyFlatSpec with Matchers { + "JSpecifiedGen" should "be duplicated correctly" in { + val lit = Literal.Text("foo", null, new MetadataStorage()) + val callArg = new JSpecified(true, None, lit) + callArg should not be null + + val dupl = callArg.duplicate(false, false, false, false) + dupl.value() shouldEqual lit + } + + "JSpecifiedGen" should "have generated parameter names with javac compiler" in { + val lit = Literal.Text("foo", null, new MetadataStorage()) + val callArg = new JSpecified(isSynthetic = true, value = lit, name = None) + callArg should not be null + } + + "JSpecifiedGen" should "have overridden toString method" in { + val lit = Literal.Text("foo", null, new MetadataStorage()) + val callArg = new JSpecified(true, None, lit) + val str = callArg.toString + withClue(s"String representation: " + str) { + str.contains("JCallArgument.JSpecified") shouldBe true + str.contains("name = None") shouldBe true + str.contains("value = Literal.Text") shouldBe true + str.contains("location = null") shouldBe true + } + } +} diff --git a/engine/runtime-parser-processor/src/main/java/module-info.java b/engine/runtime-parser-processor/src/main/java/module-info.java new file mode 100644 index 000000000000..91a6d8e6532b --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/module-info.java @@ -0,0 +1,7 @@ +module org.enso.runtime.parser.processor { + requires java.compiler; + requires org.enso.runtime.parser.dsl; + + provides javax.annotation.processing.Processor with + org.enso.runtime.parser.processor.IRProcessor; +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/ClassField.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/ClassField.java new file mode 100644 index 000000000000..cb2d18723ed6 --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/ClassField.java @@ -0,0 +1,129 @@ +package org.enso.runtime.parser.processor; + +import java.util.Objects; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.type.TypeMirror; +import org.enso.runtime.parser.processor.utils.Utils; + +/** Declared field in the generated class. */ +public final class ClassField { + + private final String modifiers; + private final TypeMirror type; + private final String name; + private final String initializer; + private final boolean canBeNull; + private final ProcessingEnvironment procEnv; + + public static Builder builder() { + return new Builder(); + } + + /** + * @param modifiers e.g. "private final" + * @param initializer Initial value of the field. Can be, e.g., {@code "null"}. + */ + private ClassField( + String modifiers, + TypeMirror type, + String name, + String initializer, + boolean canBeNull, + ProcessingEnvironment procEnv) { + this.modifiers = modifiers; + this.type = type; + this.name = name; + this.initializer = initializer; + this.canBeNull = canBeNull; + this.procEnv = procEnv; + } + + public String name() { + return name; + } + + public String modifiers() { + return modifiers; + } + + public TypeMirror getType() { + return type; + } + + public String getTypeName() { + return type.toString(); + } + + public String getSimpleTypeName() { + return Utils.simpleTypeName(type); + } + + public boolean isPrimitive() { + return type.getKind().isPrimitive(); + } + + /** + * @return May be null. In that case, initializer is unknown. Note that the class field can be + * primitive. + */ + public String initializer() { + return initializer; + } + + public boolean canBeNull() { + return canBeNull; + } + + @Override + public String toString() { + return modifiers + " " + type + " " + name; + } + + public static final class Builder { + private TypeMirror type; + private String name; + private String modifiers = null; + private String initializer = null; + private Boolean canBeNull = null; + private ProcessingEnvironment procEnv; + + public Builder modifiers(String modifiers) { + this.modifiers = modifiers; + return this; + } + + public Builder type(TypeMirror type) { + this.type = type; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder canBeNull(boolean canBeNull) { + this.canBeNull = canBeNull; + return this; + } + + public Builder initializer(String initializer) { + this.initializer = initializer; + return this; + } + + public Builder procEnv(ProcessingEnvironment procEnv) { + this.procEnv = procEnv; + return this; + } + + public ClassField build() { + Objects.requireNonNull(type); + Objects.requireNonNull(name); + Objects.requireNonNull(procEnv); + Objects.requireNonNull(canBeNull); + var modifiers = this.modifiers != null ? this.modifiers : ""; + return new ClassField(modifiers, type, name, initializer, canBeNull, procEnv); + } + } +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/GenerateIRAnnotationVisitor.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/GenerateIRAnnotationVisitor.java new file mode 100644 index 000000000000..7010f9d590c9 --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/GenerateIRAnnotationVisitor.java @@ -0,0 +1,71 @@ +package org.enso.runtime.parser.processor; + +import java.util.LinkedHashSet; +import java.util.List; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.SimpleAnnotationValueVisitor14; +import org.enso.runtime.parser.dsl.GenerateIR; +import org.enso.runtime.parser.processor.utils.Utils; + +final class GenerateIRAnnotationVisitor extends SimpleAnnotationValueVisitor14 { + private final ProcessingEnvironment procEnv; + private final ExecutableElement annotationField; + private final LinkedHashSet allInterfaces = new LinkedHashSet<>(); + private TypeElement irInterface; + + GenerateIRAnnotationVisitor(ProcessingEnvironment procEnv, ExecutableElement annotationField) { + this.procEnv = procEnv; + this.annotationField = annotationField; + allInterfaces.add(Utils.irTypeElement(procEnv)); + } + + @Override + public Void visitArray(List vals, Void unused) { + for (var val : vals) { + val.accept(this, null); + } + return null; + } + + @Override + public Void visitType(TypeMirror t, Void unused) { + var typeElem = (TypeElement) procEnv.getTypeUtils().asElement(t); + if (Utils.isSubtypeOfIR(typeElem, procEnv)) { + if (irInterface != null) { + throw new IRProcessingException( + "Only one interface can be specified as the IR interface, but found multiple: " + + irInterface + + " and " + + typeElem, + annotationField); + } + irInterface = typeElem; + } + allInterfaces.add(typeElem); + return null; + } + + /** + * Returns list of all the interfaces specified in {@link GenerateIR#interfaces()}. May be empty. + */ + public List getAllInterfaces() { + return allInterfaces.stream().toList(); + } + + /** + * Returns a type from {@link GenerateIR#interfaces()} that is a subtype of {@code + * org.enso.compiler.core.IR}. There must be only one such subtype specified. + * + * @return If there is no interface that is a subtype of {@code org.enso.compiler.core.IR} in the + * {@link GenerateIR#interfaces()}, returns {@code null}. Otherwise, returns the interface. + * Note that if null is returned, {@code org.enso.compiler.core.IR} should be used. See {@link + * GenerateIR#interfaces()}. + */ + public TypeElement getIrInterface() { + return irInterface; + } +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/GeneratedClassContext.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/GeneratedClassContext.java new file mode 100644 index 000000000000..71a02b5ab702 --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/GeneratedClassContext.java @@ -0,0 +1,229 @@ +package org.enso.runtime.parser.processor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.SimpleTypeVisitor14; +import org.enso.runtime.parser.processor.field.Field; +import org.enso.runtime.parser.processor.utils.Utils; + +/** + * A context created for the generated class. Everything that is needed for the code generation of a + * single class is contained in this class. + */ +public final class GeneratedClassContext { + private final String className; + private final List userFields; + private final List allFields; + private final List constructorParameters; + private final ProcessingEnvironment processingEnvironment; + private final ProcessedClass processedClass; + + private final ClassField diagnosticsMetaField; + private final ClassField passDataMetaField; + private final ClassField locationMetaField; + private final ClassField idMetaField; + + /** Meta fields are always present in the generated class. */ + private final List metaFields; + + /** + * @param className Simple name of the generated class + * @param userFields List of user defined fields. These fields are collected from parameterless + * abstract methods in the interface. + */ + GeneratedClassContext( + String className, + List userFields, + ProcessingEnvironment processingEnvironment, + ProcessedClass processedClass) { + this.className = Objects.requireNonNull(className); + this.userFields = Objects.requireNonNull(userFields); + this.processingEnvironment = Objects.requireNonNull(processingEnvironment); + this.processedClass = processedClass; + ensureSimpleName(className); + + this.diagnosticsMetaField = + ClassField.builder() + .modifiers("protected") + .type(Utils.diagnosticStorageTypeElement(processingEnvironment).asType()) + .name("diagnostics") + .procEnv(processingEnvironment) + .canBeNull(true) + .build(); + this.passDataMetaField = + ClassField.builder() + .modifiers("protected") + .type(Utils.metadataStorageTypeElement(processingEnvironment).asType()) + .name("passData") + .initializer("new MetadataStorage()") + .procEnv(processingEnvironment) + .canBeNull(false) + .build(); + this.locationMetaField = + ClassField.builder() + .modifiers("protected") + .type(Utils.identifiedLocationTypeElement(processingEnvironment).asType()) + .name("location") + .procEnv(processingEnvironment) + .canBeNull(true) + .build(); + this.idMetaField = + ClassField.builder() + .modifiers("protected") + .type(Utils.uuidTypeElement(processingEnvironment).asType()) + .name("id") + .canBeNull(true) + .procEnv(processingEnvironment) + .build(); + this.metaFields = + List.of(diagnosticsMetaField, passDataMetaField, locationMetaField, idMetaField); + + this.allFields = new ArrayList<>(metaFields); + for (var userField : userFields) { + allFields.add( + ClassField.builder() + .modifiers("private final") + .type(userField.getType()) + .name(userField.getName()) + .canBeNull(userField.isNullable() && !userField.isPrimitive()) + .procEnv(processingEnvironment) + .build()); + } + this.constructorParameters = + allFields.stream() + .map(classField -> new Parameter(classField.getType(), classField.name())) + .toList(); + } + + private static void ensureSimpleName(String name) { + if (name.contains(".")) { + throw new IRProcessingException("Class name must be simple, not qualified", null); + } + } + + public ClassField getLocationMetaField() { + return locationMetaField; + } + + public ClassField getPassDataMetaField() { + return passDataMetaField; + } + + public ClassField getDiagnosticsMetaField() { + return diagnosticsMetaField; + } + + public ClassField getIdMetaField() { + return idMetaField; + } + + public List getUserFields() { + return userFields; + } + + /** Returns simple name of the class that is being generated. */ + public String getClassName() { + return className; + } + + public ProcessedClass getProcessedClass() { + return processedClass; + } + + List getMetaFields() { + return metaFields; + } + + /** Returns list of all fields in the generated class - meta field and user-defined fields. */ + public List getAllFields() { + return allFields; + } + + public ProcessingEnvironment getProcessingEnvironment() { + return processingEnvironment; + } + + /** + * Returns list of parameters for the constructor of the subclass annotated with {@link + * org.enso.runtime.parser.dsl.GenerateFields}. The list is gathered from all the fields present + * in the generated super class. + * + * @see #getAllFields() + * @return List of parameters for the constructor of the subclass. A subset of all the fields in + * the generated super class. + */ + public List getSubclassConstructorParameters() { + var ctor = processedClass.getCtor(); + var ctorParams = new ArrayList(); + for (var param : ctor.getParameters()) { + var paramType = param.asType().toString(); + var paramName = param.getSimpleName().toString(); + var fieldsWithSameType = + allFields.stream().filter(field -> paramType.equals(field.getTypeName())).toList(); + if (fieldsWithSameType.isEmpty()) { + throw noMatchingFieldError(param); + } else if (fieldsWithSameType.size() == 1) { + ctorParams.add(fieldsWithSameType.get(0)); + } else { + // There are multiple fields with the same type - try to match on the name + var fieldsWithSameName = + fieldsWithSameType.stream().filter(field -> paramName.equals(field.name())).toList(); + Utils.hardAssert( + fieldsWithSameName.size() < 2, + "Cannot have more than one field with the same name and type"); + if (fieldsWithSameName.isEmpty()) { + throw noMatchingFieldError(param); + } + Utils.hardAssert(fieldsWithSameName.size() == 1); + ctorParams.add(fieldsWithSameName.get(0)); + } + } + return ctorParams; + } + + private String simpleTypeName(VariableElement param) { + var paramType = param.asType(); + var typeVisitor = + new SimpleTypeVisitor14() { + @Override + public String visitDeclared(DeclaredType t, Void unused) { + return t.asElement().getSimpleName().toString(); + } + + @Override + public String visitPrimitive(PrimitiveType t, Void unused) { + return t.toString(); + } + }; + var typeName = paramType.accept(typeVisitor, null); + return typeName; + } + + private IRProcessingException noMatchingFieldError(VariableElement param) { + var paramSimpleType = simpleTypeName(param); + var paramName = param.getSimpleName().toString(); + var errMsg = + String.format( + "No matching field found for parameter %s of type %s. All fields: %s", + paramName, paramSimpleType, allFields); + return new IRProcessingException(errMsg, param); + } + + /** Method parameter */ + record Parameter(TypeMirror type, String name) { + @Override + public String toString() { + return simpleTypeName() + " " + name; + } + + String simpleTypeName() { + return Utils.simpleTypeName(type); + } + } +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/IRNodeClassGenerator.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/IRNodeClassGenerator.java new file mode 100644 index 000000000000..67959070fe55 --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/IRNodeClassGenerator.java @@ -0,0 +1,437 @@ +package org.enso.runtime.parser.processor; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.processing.ProcessingEnvironment; +import org.enso.runtime.parser.processor.field.Field; +import org.enso.runtime.parser.processor.field.FieldCollector; +import org.enso.runtime.parser.processor.methodgen.BuilderMethodGenerator; +import org.enso.runtime.parser.processor.methodgen.CopyMethodGenerator; +import org.enso.runtime.parser.processor.methodgen.DuplicateMethodGenerator; +import org.enso.runtime.parser.processor.methodgen.EqualsMethodGenerator; +import org.enso.runtime.parser.processor.methodgen.HashCodeMethodGenerator; +import org.enso.runtime.parser.processor.methodgen.MapExpressionsMethodGenerator; +import org.enso.runtime.parser.processor.methodgen.SetLocationMethodGenerator; +import org.enso.runtime.parser.processor.methodgen.ToStringMethodGenerator; +import org.enso.runtime.parser.processor.utils.Utils; + +/** + * Generates code for a super class for a class annotated with {@link + * org.enso.runtime.parser.dsl.GenerateIR}. + */ +final class IRNodeClassGenerator { + private final ProcessingEnvironment processingEnv; + private final ProcessedClass processedClass; + + /** Name of the class that is being generated */ + private final String className; + + private final GeneratedClassContext generatedClassContext; + private final DuplicateMethodGenerator duplicateMethodGenerator; + private final CopyMethodGenerator copyMethodGenerator; + private final SetLocationMethodGenerator setLocationMethodGenerator; + private final BuilderMethodGenerator builderMethodGenerator; + private final MapExpressionsMethodGenerator mapExpressionsMethodGenerator; + private final EqualsMethodGenerator equalsMethodGenerator; + private final HashCodeMethodGenerator hashCodeMethodGenerator; + private final ToStringMethodGenerator toStringMethodGenerator; + + private static final Set defaultImportedTypes = + Set.of( + "java.util.UUID", + "java.util.ArrayList", + "java.util.function.Function", + "java.util.Objects", + "org.enso.compiler.core.Identifier", + "org.enso.compiler.core.IR", + "org.enso.compiler.core.ir.DiagnosticStorage", + "org.enso.compiler.core.ir.DiagnosticStorage$", + "org.enso.compiler.core.ir.Expression", + "org.enso.compiler.core.ir.IdentifiedLocation", + "org.enso.compiler.core.ir.MetadataStorage", + "scala.Option"); + + /** + * @param className Name of the generated class. Non qualified. + */ + IRNodeClassGenerator( + ProcessingEnvironment processingEnv, ProcessedClass processedClass, String className) { + Utils.hardAssert(!className.contains("."), "Class name should be simple, not qualified"); + this.processingEnv = processingEnv; + this.processedClass = processedClass; + this.className = className; + var userFields = getAllUserFields(processedClass); + var duplicateMethod = + Utils.findDuplicateMethod(processedClass.getIrInterfaceElem(), processingEnv); + this.generatedClassContext = + new GeneratedClassContext(className, userFields, processingEnv, processedClass); + this.duplicateMethodGenerator = + new DuplicateMethodGenerator(duplicateMethod, generatedClassContext); + this.copyMethodGenerator = new CopyMethodGenerator(generatedClassContext); + this.builderMethodGenerator = new BuilderMethodGenerator(generatedClassContext); + var mapExpressionsMethod = + Utils.findMapExpressionsMethod(processedClass.getIrInterfaceElem(), processingEnv); + this.mapExpressionsMethodGenerator = + new MapExpressionsMethodGenerator(mapExpressionsMethod, generatedClassContext); + var setLocationMethod = + Utils.findMethod( + processedClass.getIrInterfaceElem(), + processingEnv, + method -> method.getSimpleName().toString().equals("setLocation")); + this.setLocationMethodGenerator = + new SetLocationMethodGenerator(setLocationMethod, generatedClassContext); + this.equalsMethodGenerator = new EqualsMethodGenerator(generatedClassContext); + this.hashCodeMethodGenerator = new HashCodeMethodGenerator(generatedClassContext); + this.toStringMethodGenerator = new ToStringMethodGenerator(generatedClassContext); + } + + /** Returns simple name of the generated class. */ + String getClassName() { + return className; + } + + /** Returns set of import statements that should be included in the generated class. */ + Set imports() { + var importsForFields = + generatedClassContext.getUserFields().stream() + .flatMap(field -> field.getImportedTypes().stream()) + .collect(Collectors.toUnmodifiableSet()); + var allImports = new HashSet(); + allImports.addAll(defaultImportedTypes); + allImports.addAll(importsForFields); + return allImports.stream() + .map(importedType -> "import " + importedType + ";") + .collect(Collectors.toUnmodifiableSet()); + } + + /** Generates the body of the class - fields, field setters, method overrides, builder, etc. */ + String classBody() { + var code = + """ + $fields + + $defaultCtor + + $validateConstructor + + public static Builder builder() { + return new Builder(); + } + + $copyMethod + + $userDefinedGetters + + $overrideIRMethods + + $mapExpressionsMethod + + $equalsMethod + + $hashCodeMethod + + $toStringMethod + + $builder + """ + .replace("$fields", fieldsCode()) + .replace("$defaultCtor", defaultConstructor()) + .replace("$validateConstructor", validateConstructor()) + .replace("$copyMethod", copyMethodGenerator.generateMethodCode()) + .replace("$userDefinedGetters", userDefinedGetters()) + .replace("$overrideIRMethods", overrideIRMethods()) + .replace("$mapExpressionsMethod", mapExpressions()) + .replace("$equalsMethod", equalsMethodGenerator.generateMethodCode()) + .replace("$hashCodeMethod", hashCodeMethodGenerator.generateMethodCode()) + .replace("$toStringMethod", toStringMethodGenerator.generateMethodCode()) + .replace("$builder", builderMethodGenerator.generateBuilder()); + return Utils.indent(code, 2); + } + + private List getAllUserFields(ProcessedClass processedClass) { + var fieldCollector = new FieldCollector(processingEnv, processedClass); + return fieldCollector.collectFields(); + } + + /** + * Returns string representation of the class fields. Meant to be at the beginning of the class + * body. + */ + private String fieldsCode() { + var userDefinedFields = + generatedClassContext.getUserFields().stream() + .map(field -> "private final " + field.getSimpleTypeName() + " " + field.getName()) + .collect(Collectors.joining(";" + System.lineSeparator())); + var code = + """ + $userDefinedFields; + // The following meta fields cannot be private, as we are explicitly + // setting them in the `duplicate` method. Inheritor should not access + // these fields directly + protected DiagnosticStorage diagnostics; + protected MetadataStorage passData; + protected IdentifiedLocation location; + protected UUID id; + """ + .replace("$userDefinedFields", userDefinedFields); + return code; + } + + /** + * Returns string representation of the protected constructor of the generated class. The default + * constructor has parameters for both meta fields and user-defined fields. + */ + private String defaultConstructor() { + var docs = + """ + /** + * Default constructor matching the signature of subtype's constructor. + * The rest of the fields not specified as parameters to this constructor are initialized to + * their default value. + */ + """; + var subclassCtorParams = generatedClassContext.getSubclassConstructorParameters(); + var allFields = generatedClassContext.getAllFields(); + var diff = Utils.diff(allFields, subclassCtorParams); + var ctorCode = constructorForFields(subclassCtorParams, diff); + return docs + ctorCode; + } + + /** + * The caller must ensure that parameters and {@code initializeToNull} are disjoint sets and that + * the union of them is equal to the set of all fields in the generated class. + * + * @param parameters Fields that will be parameters of the constructor. Can be empty list. + * @param initializeToNull Rest of the fields that will be initialized to null in the constructor. + * Can be empty list. + */ + private String constructorForFields( + List parameters, List initializeToNull) { + Utils.hardAssert( + !(parameters.isEmpty() && initializeToNull.isEmpty()), + "At least one of the list must be non empty"); + var sb = new StringBuilder(); + sb.append("protected ").append(className).append("("); + var inParens = + parameters.stream() + .map( + consParam -> + "$consType $consName" + .replace("$consType", consParam.getSimpleTypeName()) + .replace("$consName", consParam.name())) + .collect(Collectors.joining(", ")); + sb.append(inParens).append(") {").append(System.lineSeparator()); + + if (!parameters.isEmpty()) { + var ctorBody = + parameters.stream() + .map(field -> " this.$fieldName = $fieldName;".replace("$fieldName", field.name())) + .collect(Collectors.joining(System.lineSeparator())); + sb.append(ctorBody); + } + sb.append(System.lineSeparator()); + + // The rest of the constructor body initializes the rest of the fields to null. + if (!initializeToNull.isEmpty()) { + var initToNullBody = + initializeToNull.stream() + .map( + field -> { + var initializer = field.initializer() != null ? field.initializer() : "null"; + return " this.$fieldName = $init;" + .replace("$fieldName", field.name()) + .replace("$init", initializer); + }) + .collect(Collectors.joining(System.lineSeparator())); + sb.append(initToNullBody); + sb.append(System.lineSeparator()); + } + sb.append(" validateConstructor();").append(System.lineSeparator()); + sb.append(System.lineSeparator()); + sb.append("}").append(System.lineSeparator()); + return sb.toString(); + } + + /** + * Generates code for validation at the end of the constructor. Validates if all the required + * fields were set in the constructor (passed as params). + */ + private String validateConstructor() { + var sb = new StringBuilder(); + sb.append( + """ + /** + * Validates if all the required fields were set in the constructor. + */ + """); + sb.append("private void validateConstructor() {").append(System.lineSeparator()); + var checkCode = + generatedClassContext.getAllFields().stream() + .filter(field -> !field.canBeNull()) + .filter(field -> !field.isPrimitive()) + .map( + notNullField -> + """ + if ($fieldName == null) { + throw new IllegalArgumentException("$fieldName is required"); + } + """ + .replace("$fieldName", notNullField.name())) + .collect(Collectors.joining(System.lineSeparator())); + sb.append(Utils.indent(checkCode, 2)); + sb.append(System.lineSeparator()); + sb.append("}").append(System.lineSeparator()); + return sb.toString(); + } + + private String childrenMethodBody() { + var sb = new StringBuilder(); + var nl = System.lineSeparator(); + sb.append("var list = new ArrayList();").append(nl); + generatedClassContext.getUserFields().stream() + .filter(Field::isChild) + .forEach( + childField -> { + String addToListCode; + if (childField.isList()) { + addToListCode = + """ + $childName.foreach(list::add); + """ + .replace("$childName", childField.getName()); + } else if (childField.isOption()) { + addToListCode = + """ + if ($childName.isDefined()) { + list.add($childName.get()); + } + """ + .replace("$childName", childField.getName()); + } else { + addToListCode = "list.add(" + childField.getName() + ");"; + } + + var childName = childField.getName(); + if (childField.isNullable()) { + sb.append( + """ + if ($childName != null) { + $addToListCode + } + """ + .replace("$childName", childName) + .replace("$addToListCode", addToListCode)); + } else { + sb.append(addToListCode).append(nl); + } + }); + sb.append("return scala.jdk.javaapi.CollectionConverters.asScala(list).toList();").append(nl); + return indent(sb.toString(), 2); + } + + /** + * Returns a String representing all the overriden methods from {@code org.enso.compiler.core.IR}. + * Meant to be inside the generated record definition. + */ + private String overrideIRMethods() { + var code = + """ + + @Override + public MetadataStorage passData() { + assert passData != null : "passData must always be initialized"; + return passData; + } + + @Override + public Option location() { + if (location == null) { + return scala.Option.empty(); + } else { + return scala.Option.apply(location); + } + } + + $setLocationMethod + + @Override + public IdentifiedLocation identifiedLocation() { + return this.location; + } + + @Override + public scala.collection.immutable.List children() { + $childrenMethodBody + } + + @Override + public @Identifier UUID getId() { + if (id == null) { + id = UUID.randomUUID(); + } + return id; + } + + @Override + public DiagnosticStorage diagnostics() { + return diagnostics; + } + + @Override + public DiagnosticStorage getDiagnostics() { + if (diagnostics == null) { + diagnostics = DiagnosticStorage$.MODULE$.createEmpty(); + } + return diagnostics; + } + + public DiagnosticStorage diagnosticsCopy() { + if (diagnostics == null) { + return null; + } else { + return diagnostics.copy(); + } + } + + $duplicateMethods + + @Override + public String showCode(int indent) { + throw new UnsupportedOperationException("unimplemented"); + } + """ + .replace("$childrenMethodBody", childrenMethodBody()) + .replace("$setLocationMethod", setLocationMethodGenerator.generateMethodCode()) + .replace("$duplicateMethods", duplicateMethodGenerator.generateDuplicateMethodsCode()); + return code; + } + + /** Returns string representation of all getters for the user-defined fields. */ + private String userDefinedGetters() { + var code = + generatedClassContext.getUserFields().stream() + .map( + field -> + """ + public $returnType $fieldName() { + return $fieldName; + } + """ + .replace("$returnType", field.getSimpleTypeName()) + .replace("$fieldName", field.getName())) + .collect(Collectors.joining(System.lineSeparator())); + return code; + } + + private String mapExpressions() { + return mapExpressionsMethodGenerator.generateMapExpressionsMethodCode(); + } + + private static String indent(String code, int indentation) { + return code.lines() + .map(line -> " ".repeat(indentation) + line) + .collect(Collectors.joining(System.lineSeparator())); + } +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/IRProcessingException.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/IRProcessingException.java new file mode 100644 index 000000000000..3956ae44d482 --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/IRProcessingException.java @@ -0,0 +1,25 @@ +package org.enso.runtime.parser.processor; + +import javax.lang.model.element.Element; + +/** + * This exception should be contained only in IR element processing. It is caught in the main + * processing loop in {@link IRProcessor}. + */ +public final class IRProcessingException extends RuntimeException { + private final Element element; + + public IRProcessingException(String message, Element element, Throwable cause) { + super(message, cause); + this.element = element; + } + + public IRProcessingException(String message, Element element) { + super(message); + this.element = element; + } + + public Element getElement() { + return element; + } +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/IRProcessor.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/IRProcessor.java new file mode 100644 index 000000000000..1138b2874190 --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/IRProcessor.java @@ -0,0 +1,262 @@ +package org.enso.runtime.parser.processor; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeKind; +import javax.tools.Diagnostic.Kind; +import javax.tools.JavaFileObject; +import org.enso.runtime.parser.dsl.GenerateFields; +import org.enso.runtime.parser.dsl.GenerateIR; +import org.enso.runtime.parser.processor.utils.Utils; + +@SupportedAnnotationTypes({ + "org.enso.runtime.parser.dsl.GenerateIR", + "org.enso.runtime.parser.dsl.IRChild", + "org.enso.runtime.parser.dsl.IRCopyMethod", +}) +public class IRProcessor extends AbstractProcessor { + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latest(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + var generateIRElems = roundEnv.getElementsAnnotatedWith(GenerateIR.class); + for (var generateIRElem : generateIRElems) { + try { + ensureIsClass(generateIRElem); + processGenerateIRElem((TypeElement) generateIRElem); + } catch (IRProcessingException e) { + Element element; + if (e.getElement() != null) { + element = e.getElement(); + } else { + element = generateIRElem; + } + processingEnv.getMessager().printMessage(Kind.ERROR, e.getMessage(), element); + return false; + } + } + return true; + } + + /** + * @param processedClassElem Class annotated with {@link GenerateIR}. + */ + private void processGenerateIRElem(TypeElement processedClassElem) { + ensureIsPublicFinal(processedClassElem); + ensureEnclosedInInterfaceOrPackage(processedClassElem); + ensureHasSingleAnnotatedConstructor(processedClassElem); + ensureExtendsGeneratedSuperclass(processedClassElem); + + var processedClass = constructProcessedClass(processedClassElem); + var pkgName = packageName(processedClassElem); + var newClassName = generatedClassName(processedClassElem); + String newBinaryName; + if (!pkgName.isEmpty()) { + newBinaryName = pkgName + "." + newClassName; + } else { + newBinaryName = newClassName; + } + + JavaFileObject srcGen; + try { + srcGen = processingEnv.getFiler().createSourceFile(newBinaryName, processedClassElem); + } catch (IOException e) { + throw new IRProcessingException( + "Failed to create source file for IRNode", processedClassElem, e); + } + + String generatedCode; + var classGenerator = new IRNodeClassGenerator(processingEnv, processedClass, newClassName); + generatedCode = generateSingleNodeClass(classGenerator, processedClass, pkgName); + + try { + try (var lineWriter = new PrintWriter(srcGen.openWriter())) { + lineWriter.write(generatedCode); + } + } catch (IOException e) { + throw new IRProcessingException( + "Failed to write to source file for IRNode", processedClassElem, e); + } + } + + private String generatedClassName(TypeElement processedClassElem) { + var superClass = processedClassElem.getSuperclass(); + if (superClass.getKind() == TypeKind.ERROR) { + // The super class does not yet exist + return superClass.toString(); + } else if (superClass.getKind() == TypeKind.DECLARED) { + var superClassElem = (TypeElement) processingEnv.getTypeUtils().asElement(superClass); + return superClassElem.getSimpleName().toString(); + } else { + throw new IRProcessingException( + "Super class must be a declared type", + processingEnv.getTypeUtils().asElement(superClass)); + } + } + + private ProcessedClass constructProcessedClass(TypeElement processedClassElem) { + // GenerateIR.interfaces cannot be accessed directly, we have to access the + // classes via type mirrors. + TypeElement irIfaceToImplement = Utils.irTypeElement(processingEnv); + List allInterfacesToImplement = List.of(); + for (var annotMirror : processedClassElem.getAnnotationMirrors()) { + if (annotMirror.getAnnotationType().toString().equals(GenerateIR.class.getName())) { + var annotMirrorElemValues = + processingEnv.getElementUtils().getElementValuesWithDefaults(annotMirror); + for (var entry : annotMirrorElemValues.entrySet()) { + if (entry.getKey().getSimpleName().toString().equals("interfaces")) { + var annotValueVisitor = new GenerateIRAnnotationVisitor(processingEnv, entry.getKey()); + entry.getValue().accept(annotValueVisitor, null); + if (annotValueVisitor.getIrInterface() != null) { + irIfaceToImplement = annotValueVisitor.getIrInterface(); + } + allInterfacesToImplement = annotValueVisitor.getAllInterfaces(); + } + } + } + } + Utils.hardAssert(irIfaceToImplement != null); + if (!Utils.isSubtypeOfIR(irIfaceToImplement, processingEnv)) { + throw new IRProcessingException( + "Interface to implement must be a subtype of IR interface", irIfaceToImplement); + } + var annotatedCtor = getAnnotatedCtor(processedClassElem); + var processedClass = + new ProcessedClass( + processedClassElem, annotatedCtor, irIfaceToImplement, allInterfacesToImplement); + return processedClass; + } + + private void ensureIsClass(Element elem) { + if (elem.getKind() != ElementKind.CLASS) { + throw new IRProcessingException("GenerateIR annotation can only be applied to classes", elem); + } + } + + private void ensureIsPublicFinal(TypeElement clazz) { + if (!clazz.getModifiers().contains(Modifier.FINAL) + || !clazz.getModifiers().contains(Modifier.PUBLIC)) { + throw new IRProcessingException( + "Class annotated with @GenerateIR must be public final", clazz); + } + } + + private void ensureEnclosedInInterfaceOrPackage(TypeElement clazz) { + var enclosingElem = clazz.getEnclosingElement(); + if (enclosingElem != null) { + if (!(enclosingElem.getKind() == ElementKind.PACKAGE + || enclosingElem.getKind() == ElementKind.INTERFACE)) { + throw new IRProcessingException( + "Class annotated with @GenerateIR must be enclosed in a package or an interface", + clazz); + } + } + } + + private void ensureHasSingleAnnotatedConstructor(TypeElement clazz) { + var annotatedCtorsCnt = + clazz.getEnclosedElements().stream() + .filter(elem -> elem.getKind() == ElementKind.CONSTRUCTOR) + .filter(ctor -> ctor.getAnnotation(GenerateFields.class) != null) + .count(); + if (annotatedCtorsCnt != 1) { + throw new IRProcessingException( + "Class annotated with @GenerateIR must have exactly one constructor annotated with" + + " @GenerateFields", + clazz); + } + } + + private void ensureExtendsGeneratedSuperclass(TypeElement clazz) { + var superClass = clazz.getSuperclass(); + if (superClass.getKind() == TypeKind.NONE || superClass.toString().equals("java.lang.Object")) { + throw new IRProcessingException( + "Class annotated with @GenerateIR must have 'extends' clause", clazz); + } + } + + private static ExecutableElement getAnnotatedCtor(TypeElement clazz) { + // It should already be ensured that there is only a single annotated constructor in the class, + // hence the AssertionError + return clazz.getEnclosedElements().stream() + .filter(elem -> elem.getAnnotation(GenerateFields.class) != null) + .map(elem -> (ExecutableElement) elem) + .findFirst() + .orElseThrow( + () -> new IRProcessingException("No constructor annotated with GenerateFields", clazz)); + } + + private String packageName(Element elem) { + var pkg = processingEnv.getElementUtils().getPackageOf(elem); + return pkg.getQualifiedName().toString(); + } + + /** + * Generates code for a super class. + * + * @param pkgName Package of the current processed class. + * @return The generated code ready to be written to a {@code .java} source. + */ + private static String generateSingleNodeClass( + IRNodeClassGenerator irNodeClassGen, ProcessedClass processedClass, String pkgName) { + var imports = + irNodeClassGen.imports().stream() + .sorted() + .collect(Collectors.joining(System.lineSeparator())); + var pkg = pkgName.isEmpty() ? "" : "package " + pkgName + ";"; + var interfaces = + processedClass.getInterfaces().stream() + .map(TypeElement::getSimpleName) + .collect(Collectors.joining(", ")); + var code = + """ + $pkg + + $imports + + $docs + abstract class $className implements $interfaces { + $classBody + } + """ + .replace("$pkg", pkg) + .replace("$imports", imports) + .replace("$docs", jdoc(processedClass)) + .replace("$className", irNodeClassGen.getClassName()) + .replace("$interfaces", interfaces) + .replace("$classBody", irNodeClassGen.classBody()); + return code; + } + + private static String jdoc(ProcessedClass processedClass) { + var thisClassName = IRProcessor.class.getName(); + var processedClassName = processedClass.getClazz().getQualifiedName().toString(); + var docs = + """ + /** + * Generated by {@code $thisClassName} IR annotation processor. + * Generated from {@link $processedClassName}. + * The {@link $processedClassName} is meant to extend this generated class. + */ + """ + .replace("$thisClassName", thisClassName) + .replace("$processedClassName", processedClassName); + return docs; + } +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/ProcessedClass.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/ProcessedClass.java new file mode 100644 index 000000000000..7ced69816fa0 --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/ProcessedClass.java @@ -0,0 +1,59 @@ +package org.enso.runtime.parser.processor; + +import java.util.List; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import org.enso.runtime.parser.dsl.GenerateIR; + +/** + * Represents a class annotated with {@link org.enso.runtime.parser.dsl.GenerateIR} that is + * currently being processed by the {@link IRProcessor}. + */ +public final class ProcessedClass { + private final TypeElement clazz; + private final ExecutableElement ctor; + private final TypeElement irInterfaceElem; + private final List interfaces; + + /** + * @param clazz Class being processed by the processor, annotated with {@link GenerateIR} + * @param ctor Constructor annotated with {@link org.enso.runtime.parser.dsl.GenerateFields}. + * @param irInterfaceElem Interface that the generated superclass must implement. Must be subtype + * of {@code org.enso.compiler.core.IR}. + * @param interfaces All interfaces to implement. See {@link GenerateIR#interfaces()}. + */ + ProcessedClass( + TypeElement clazz, + ExecutableElement ctor, + TypeElement irInterfaceElem, + List interfaces) { + this.clazz = clazz; + this.ctor = ctor; + this.irInterfaceElem = irInterfaceElem; + this.interfaces = interfaces; + } + + public TypeElement getClazz() { + return clazz; + } + + public ExecutableElement getCtor() { + return ctor; + } + + /** + * Returns the interface that the generated superclass must implement. Is a subtype of {@code + * org.enso.compiler.core.IR}. + */ + public TypeElement getIrInterfaceElem() { + return irInterfaceElem; + } + + /** + * Returns all interfaces that the generated superclass must implement. See {@link + * GenerateIR#interfaces()}. + */ + public List getInterfaces() { + return interfaces; + } +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/Field.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/Field.java new file mode 100644 index 000000000000..6b55027ad205 --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/Field.java @@ -0,0 +1,110 @@ +package org.enso.runtime.parser.processor.field; + +import java.util.List; +import java.util.function.Function; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import org.enso.runtime.parser.dsl.IRChild; +import org.enso.runtime.parser.processor.IRProcessingException; +import org.enso.runtime.parser.processor.utils.Utils; + +/** Represents a field in the generated super class. */ +public abstract class Field { + protected final TypeMirror type; + protected final String name; + private final ProcessingEnvironment procEnv; + + protected Field(TypeMirror type, String name, ProcessingEnvironment procEnv) { + this.type = type; + this.name = name; + this.procEnv = procEnv; + } + + /** Name (identifier) of the field. */ + public String getName() { + return name; + } + + /** Returns type of this field. Must not be null. */ + public TypeMirror getType() { + return type; + } + + /** + * Does not return null. If the type is generic, the type parameter is included in the name. + * Returns non-qualified name. + */ + public String getSimpleTypeName() { + return Utils.simpleTypeName(type); + } + + /** + * Returns true if this field is annotated with {@link org.enso.runtime.parser.dsl.IRChild}. + * + * @return + */ + public abstract boolean isChild(); + + /** + * Returns true if this field is child with {@link IRChild#required()} set to false. + * + * @return + */ + public abstract boolean isNullable(); + + /** + * Returns list of (fully-qualified) types that are necessary to import in order to use simple + * type names. + */ + public List getImportedTypes() { + return Utils.getImportedTypes(type); + } + + /** Returns true if this field is a scala immutable list. */ + public boolean isList() { + return Utils.isScalaList(type, procEnv); + } + + /** Returns true if this field is {@code scala.Option}. */ + public boolean isOption() { + return Utils.isScalaOption(type, procEnv); + } + + /** Returns true if the type of this field is Java primitive. */ + public boolean isPrimitive() { + return type.getKind().isPrimitive(); + } + + /** + * Returns true if this field extends {@link org.enso.compiler.core.ir.Expression}. + * + *

This is useful, e.g., for the {@link org.enso.compiler.core.IR#mapExpressions(Function)} + * method. + * + * @return true if this field extends {@link org.enso.compiler.core.ir.Expression} + */ + public boolean isExpression() { + return Utils.isSubtypeOfExpression(type, procEnv); + } + + /** Returns the type parameter, if this field is a generic type. Otherwise null. */ + public TypeElement getTypeParameter() { + if (type.getKind() == TypeKind.DECLARED) { + var declared = (DeclaredType) type; + var typeArgs = declared.getTypeArguments(); + if (typeArgs.isEmpty()) { + return null; + } else if (typeArgs.size() == 1) { + var typeArg = typeArgs.get(0); + return (TypeElement) procEnv.getTypeUtils().asElement(typeArg); + } else { + throw new IRProcessingException( + "Unexpected number of type arguments: " + typeArgs.size(), null); + } + } + return null; + } +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/FieldCollector.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/FieldCollector.java new file mode 100644 index 000000000000..2aa537e1c43a --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/FieldCollector.java @@ -0,0 +1,139 @@ +package org.enso.runtime.parser.processor.field; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import org.enso.runtime.parser.dsl.IRChild; +import org.enso.runtime.parser.dsl.IRField; +import org.enso.runtime.parser.processor.IRProcessingException; +import org.enso.runtime.parser.processor.ProcessedClass; +import org.enso.runtime.parser.processor.utils.Utils; + +/** + * Collects abstract parameterless methods from the given interface and all its superinterfaces - + * these will be represented as fields in the generated classes, hence the name. + */ +public final class FieldCollector { + private final ProcessingEnvironment processingEnv; + private final ProcessedClass processedClass; + private final TypeElement metadataStorageType; + private final TypeElement diagnosticStorageType; + private final TypeElement identifiedLocationType; + private final TypeElement uuidType; + + // Mapped by field name + private Map fields; + + public FieldCollector(ProcessingEnvironment processingEnv, ProcessedClass processedClass) { + this.processingEnv = processingEnv; + this.processedClass = processedClass; + this.metadataStorageType = Utils.metadataStorageTypeElement(processingEnv); + this.diagnosticStorageType = Utils.diagnosticStorageTypeElement(processingEnv); + this.identifiedLocationType = Utils.identifiedLocationTypeElement(processingEnv); + this.uuidType = Utils.uuidTypeElement(processingEnv); + } + + public List collectFields() { + if (fields == null) { + fields = new LinkedHashMap<>(); + collectFromCtor(); + } + return fields.values().stream().toList(); + } + + private void collectFromCtor() { + var ctor = processedClass.getCtor(); + for (var param : ctor.getParameters()) { + var paramName = param.getSimpleName().toString(); + var irFieldAnnot = param.getAnnotation(IRField.class); + var irChildAnnot = param.getAnnotation(IRChild.class); + Field field; + if (irFieldAnnot != null) { + field = processIrField(param, irFieldAnnot); + } else if (irChildAnnot != null) { + field = processIrChild(param, irChildAnnot); + } else if (Utils.hasNoAnnotations(param) && isMeta(param)) { + field = null; + } else { + var errMsg = + "Constructor parameter " + + param + + " must be annotated with either @IRField or @IRChild"; + throw new IRProcessingException(errMsg, param); + } + + if (field != null) { + fields.put(paramName, field); + } + } + } + + private boolean isMeta(VariableElement param) { + var typeUtils = processingEnv.getTypeUtils(); + return typeUtils.isSameType(param.asType(), metadataStorageType.asType()) + || typeUtils.isSameType(param.asType(), diagnosticStorageType.asType()) + || typeUtils.isSameType(param.asType(), identifiedLocationType.asType()) + || typeUtils.isSameType(param.asType(), uuidType.asType()); + } + + private Field processIrField(VariableElement param, IRField irFieldAnnot) { + var isNullable = !irFieldAnnot.required(); + var name = param.getSimpleName().toString(); + if (isPrimitiveType(param)) { + return new PrimitiveField(param.asType(), name, processingEnv); + } else { + // TODO: Assert that type is simple reference type - does not extend IR, is not generic + return new ReferenceField(processingEnv, param.asType(), name, isNullable, false); + } + } + + private Field processIrChild(VariableElement param, IRChild irChildAnnot) { + var name = param.getSimpleName().toString(); + var type = getParamType(param); + var isNullable = !irChildAnnot.required(); + if (Utils.isScalaList(param.asType(), processingEnv)) { + ensureTypeArgIsSubtypeOfIR(param.asType()); + return new ListField(name, param.asType(), processingEnv); + } else if (Utils.isScalaOption(param.asType(), processingEnv)) { + ensureTypeArgIsSubtypeOfIR(param.asType()); + return new OptionField(name, param.asType(), processingEnv); + } else { + if (!Utils.isSubtypeOfIR(type, processingEnv)) { + throw new IRProcessingException( + "Constructor parameter annotated with @IRChild must be a subtype of IR interface. " + + "Actual type is: " + + type, + param); + } + return new ReferenceField(processingEnv, param.asType(), name, isNullable, true); + } + } + + private void ensureTypeArgIsSubtypeOfIR(TypeMirror typeMirror) { + var declaredType = (DeclaredType) typeMirror; + Utils.hardAssert(declaredType.getTypeArguments().size() == 1); + var typeArg = declaredType.getTypeArguments().get(0); + var typeArgElem = (TypeElement) processingEnv.getTypeUtils().asElement(typeArg); + ensureIsSubtypeOfIR(typeArgElem); + } + + private static boolean isPrimitiveType(VariableElement ctorParam) { + return ctorParam.asType().getKind().isPrimitive(); + } + + private TypeElement getParamType(VariableElement param) { + return (TypeElement) processingEnv.getTypeUtils().asElement(param.asType()); + } + + private void ensureIsSubtypeOfIR(TypeElement typeElem) { + if (!Utils.isSubtypeOfIR(typeElem, processingEnv)) { + throw new IRProcessingException( + "Method annotated with @IRChild must return a subtype of IR interface", typeElem); + } + } +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/ListField.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/ListField.java new file mode 100644 index 000000000000..1017e09683ce --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/ListField.java @@ -0,0 +1,26 @@ +package org.enso.runtime.parser.processor.field; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.type.TypeMirror; + +/** Represents a {@code scala.collection.immutable.List} field in the IR node. */ +final class ListField extends Field { + ListField(String name, TypeMirror type, ProcessingEnvironment procEnv) { + super(type, name, procEnv); + } + + @Override + public boolean isList() { + return true; + } + + @Override + public boolean isChild() { + return true; + } + + @Override + public boolean isNullable() { + return false; + } +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/OptionField.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/OptionField.java new file mode 100644 index 000000000000..e3c846eaa0ee --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/OptionField.java @@ -0,0 +1,27 @@ +package org.enso.runtime.parser.processor.field; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.type.TypeMirror; + +/** Field representing {@code scala.Option} */ +public final class OptionField extends Field { + + public OptionField(String name, TypeMirror type, ProcessingEnvironment procEnv) { + super(type, name, procEnv); + } + + @Override + public boolean isOption() { + return true; + } + + @Override + public boolean isChild() { + return true; + } + + @Override + public boolean isNullable() { + return false; + } +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/PrimitiveField.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/PrimitiveField.java new file mode 100644 index 000000000000..9f7b7254dbc5 --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/PrimitiveField.java @@ -0,0 +1,26 @@ +package org.enso.runtime.parser.processor.field; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.type.TypeMirror; + +final class PrimitiveField extends Field { + + PrimitiveField(TypeMirror type, String name, ProcessingEnvironment procEnv) { + super(type, name, procEnv); + } + + @Override + public boolean isChild() { + return false; + } + + @Override + public boolean isNullable() { + return false; + } + + @Override + public boolean isPrimitive() { + return true; + } +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/ReferenceField.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/ReferenceField.java new file mode 100644 index 000000000000..7153f13bdf09 --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/ReferenceField.java @@ -0,0 +1,30 @@ +package org.enso.runtime.parser.processor.field; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.type.TypeMirror; + +final class ReferenceField extends Field { + private final boolean nullable; + private final boolean isChild; + + ReferenceField( + ProcessingEnvironment procEnv, + TypeMirror type, + String name, + boolean nullable, + boolean isChild) { + super(type, name, procEnv); + this.nullable = nullable; + this.isChild = isChild; + } + + @Override + public boolean isChild() { + return isChild; + } + + @Override + public boolean isNullable() { + return nullable; + } +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/BuilderMethodGenerator.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/BuilderMethodGenerator.java new file mode 100644 index 000000000000..53148cbb6efa --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/BuilderMethodGenerator.java @@ -0,0 +1,119 @@ +package org.enso.runtime.parser.processor.methodgen; + +import java.util.stream.Collectors; +import org.enso.runtime.parser.processor.ClassField; +import org.enso.runtime.parser.processor.GeneratedClassContext; +import org.enso.runtime.parser.processor.utils.Utils; + +/** + * Code generator for builder. Builder is a nested static class inside the generated class. Builder + * has a validation code that is invoked in {@code build()} method that ensures that all the + * required fields are set. Builder has a copy constructor - a constructor that takes the generated + * class object and prefills all the fields with the values from the object. This copy constructor + * is called from either the {@code duplicate} method or from copy methods. + */ +public class BuilderMethodGenerator { + private final GeneratedClassContext generatedClassContext; + + public BuilderMethodGenerator(GeneratedClassContext generatedClassContext) { + this.generatedClassContext = generatedClassContext; + } + + public String generateBuilder() { + var fieldDeclarations = + generatedClassContext.getAllFields().stream() + .map( + field -> { + var initializer = field.initializer() != null ? " = " + field.initializer() : ""; + return "private $type $name $initializer;" + .replace("$type", field.getSimpleTypeName()) + .replace("$name", field.name()) + .replace("$initializer", initializer); + }) + .collect(Collectors.joining(System.lineSeparator())); + + var fieldSetters = + generatedClassContext.getAllFields().stream() + .map( + field -> + """ + public Builder $fieldName($fieldType $fieldName) { + this.$fieldName = $fieldName; + return this; + } + """ + .replace("$fieldName", field.name()) + .replace("$fieldType", field.getSimpleTypeName())) + .collect(Collectors.joining(System.lineSeparator())); + + // Validation code for all non-nullable user fields + var validationCode = + generatedClassContext.getUserFields().stream() + .filter(field -> !field.isNullable() && !field.isPrimitive()) + .map( + field -> + """ + if (this.$fieldName == null) { + throw new IllegalArgumentException("$fieldName is required"); + } + """ + .replace("$fieldName", field.getName())) + .collect(Collectors.joining(System.lineSeparator())); + + var code = + """ + public static final class Builder { + $fieldDeclarations + + Builder() {} + + $fieldSetters + + $buildMethod + + private void validate() { + $validationCode + } + } + """ + .replace("$fieldDeclarations", Utils.indent(fieldDeclarations, 2)) + .replace("$fieldSetters", Utils.indent(fieldSetters, 2)) + .replace("$buildMethod", Utils.indent(buildMethod(), 2)) + .replace("$validationCode", Utils.indent(validationCode, 4)); + return code; + } + + private String buildMethod() { + var sb = new StringBuilder(); + var processedClassName = + generatedClassContext.getProcessedClass().getClazz().getSimpleName().toString(); + var ctorParams = generatedClassContext.getSubclassConstructorParameters(); + var ctorParamsStr = ctorParams.stream().map(ClassField::name).collect(Collectors.joining(", ")); + var fieldsNotInCtor = Utils.diff(generatedClassContext.getAllFields(), ctorParams); + sb.append("public ") + .append(processedClassName) + .append(" build() {") + .append(System.lineSeparator()); + sb.append(" ").append("validate();").append(System.lineSeparator()); + sb.append(" ") + .append(processedClassName) + .append(" result = new ") + .append(processedClassName) + .append("(") + .append(ctorParamsStr) + .append(");") + .append(System.lineSeparator()); + for (var fieldNotInCtor : fieldsNotInCtor) { + sb.append(" ") + .append("result.") + .append(fieldNotInCtor.name()) + .append(" = ") + .append(fieldNotInCtor.name()) + .append(";") + .append(System.lineSeparator()); + } + sb.append(" ").append("return result;").append(System.lineSeparator()); + sb.append("}").append(System.lineSeparator()); + return sb.toString(); + } +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/CopyMethodGenerator.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/CopyMethodGenerator.java new file mode 100644 index 000000000000..79ff2971eedf --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/CopyMethodGenerator.java @@ -0,0 +1,84 @@ +package org.enso.runtime.parser.processor.methodgen; + +import java.util.List; +import java.util.stream.Collectors; +import org.enso.runtime.parser.processor.GeneratedClassContext; + +public final class CopyMethodGenerator { + private final GeneratedClassContext ctx; + + public CopyMethodGenerator(GeneratedClassContext ctx) { + this.ctx = ctx; + } + + /** Generates the default {@code copy} method, with all the fields as parameters. */ + public String generateMethodCode() { + var docs = + """ + /** + * Creates a shallow copy of this IR element. If all of the given parameters are the + * same objects as fields, no copy is created and {@code this} is returned. + * + *

As opposed to the {@code duplicate} method, + * does not copy this IR element recursively. + */ + """; + var sb = new StringBuilder(); + sb.append(docs); + var paramList = String.join(", ", parameters()); + sb.append("public ") + .append(copyMethodRetType()) + .append(" copy(") + .append(paramList) + .append(") {") + .append(System.lineSeparator()); + sb.append(" ") + .append("boolean cond = ") + .append(cond()) + .append(";") + .append(System.lineSeparator()); + sb.append(" ").append("if (cond) {").append(System.lineSeparator()); + sb.append(" ") + .append("// One of the parameters is a different object than the field.") + .append(System.lineSeparator()); + sb.append(" ").append("var bldr = new Builder();").append(System.lineSeparator()); + for (var field : ctx.getAllFields()) { + sb.append(" ") + .append("bldr.") + .append(field.name()) + .append("(") + .append(field.name()) + .append(");") + .append(System.lineSeparator()); + } + sb.append(" ").append("return bldr.build();").append(System.lineSeparator()); + sb.append(" ").append("} else {").append(System.lineSeparator()); + sb.append(" ") + .append("return (") + .append(copyMethodRetType()) + .append(") this;") + .append(System.lineSeparator()); + sb.append(" ").append("}").append(System.lineSeparator()); + sb.append("}").append(System.lineSeparator()); + return sb.toString(); + } + + private String copyMethodRetType() { + return ctx.getProcessedClass().getClazz().getSimpleName().toString(); + } + + private List parameters() { + return ctx.getAllFields().stream() + .map(field -> field.getSimpleTypeName() + " " + field.name()) + .toList(); + } + + /** Condition expression if one of the parameters is a different object than the field. */ + private String cond() { + var inner = + ctx.getAllFields().stream() + .map(field -> "(" + field.name() + " != this." + field.name() + ")") + .collect(Collectors.joining(" || ")); + return "(" + inner + ")"; + } +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/DuplicateMethodGenerator.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/DuplicateMethodGenerator.java new file mode 100644 index 000000000000..c9b01dd168e0 --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/DuplicateMethodGenerator.java @@ -0,0 +1,352 @@ +package org.enso.runtime.parser.processor.methodgen; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import org.enso.runtime.parser.processor.GeneratedClassContext; +import org.enso.runtime.parser.processor.IRProcessingException; +import org.enso.runtime.parser.processor.field.Field; +import org.enso.runtime.parser.processor.utils.Utils; + +/** + * Code generator for {@code org.enso.compiler.core.ir.IR#duplicate} method or any of its override. + * Note that in the interface hierarchy, there can be an override with a different return type. + */ +public class DuplicateMethodGenerator { + + private final GeneratedClassContext ctx; + private final List parameters; + + /** + * @param duplicateMethod ExecutableElement representing the duplicate method (or its override). + */ + public DuplicateMethodGenerator(ExecutableElement duplicateMethod, GeneratedClassContext ctx) { + this.ctx = Objects.requireNonNull(ctx); + var boolType = ctx.getProcessingEnvironment().getTypeUtils().getPrimitiveType(TypeKind.BOOLEAN); + this.parameters = + List.of( + new Parameter(boolType, "keepLocations"), + new Parameter(boolType, "keepMetadata"), + new Parameter(boolType, "keepDiagnostics"), + new Parameter(boolType, "keepIdentifiers")); + ensureDuplicateMethodHasExpectedSignature(duplicateMethod); + } + + private void ensureDuplicateMethodHasExpectedSignature(ExecutableElement duplicateMethod) { + var dupMethodParameters = duplicateMethod.getParameters(); + if (dupMethodParameters.size() != parameters.size()) { + throw new IRProcessingException( + "Duplicate method must have " + parameters.size() + " parameters", duplicateMethod); + } + var allParamsAreBooleans = + dupMethodParameters.stream().allMatch(par -> par.asType().getKind() == TypeKind.BOOLEAN); + if (!allParamsAreBooleans) { + throw new IRProcessingException( + "All parameters of the duplicate method must be of type boolean", duplicateMethod); + } + } + + /** + * Generate code for two duplicate methods - one overridden with all four parameters, and another + * parameterless that just delegates to the first one. + */ + public String generateDuplicateMethodsCode() { + var sb = new StringBuilder(); + sb.append("@Override").append(System.lineSeparator()); + sb.append("public ") + .append(dupMethodRetType()) + .append(" duplicate(") + .append(parameters.stream().map(Parameter::toString).collect(Collectors.joining(", "))) + .append(") {") + .append(System.lineSeparator()); + var duplicatedVars = new ArrayList(); + + var duplicateMetaFieldsCode = + """ + $diagType diagnosticsDuplicated = null; + if (keepDiagnostics && this.diagnostics != null) { + diagnosticsDuplicated = this.diagnostics.copy(); + } + $metaType passDataDuplicated = null; + if (keepMetadata && this.passData != null) { + passDataDuplicated = this.passData.duplicate(); + } + $locType locationDuplicated = null; + if (keepLocations && this.location != null) { + locationDuplicated = this.location; + } + $idType idDuplicated = null; + if (keepIdentifiers && this.id != null) { + idDuplicated = this.id; + } + """ + .replace("$locType", ctx.getLocationMetaField().getSimpleTypeName()) + .replace("$metaType", ctx.getPassDataMetaField().getSimpleTypeName()) + .replace("$diagType", ctx.getDiagnosticsMetaField().getSimpleTypeName()) + .replace("$idType", ctx.getIdMetaField().getSimpleTypeName()); + sb.append(Utils.indent(duplicateMetaFieldsCode, 2)); + sb.append(System.lineSeparator()); + for (var metaVar : metaFields()) { + var dupName = metaVar.name + "Duplicated"; + duplicatedVars.add(new DuplicateVar(metaVar.type, dupName, metaVar.name, false)); + } + + for (var field : ctx.getUserFields()) { + if (field.isChild()) { + if (field.isNullable()) { + sb.append(Utils.indent(nullableChildCode(field), 2)); + sb.append(System.lineSeparator()); + duplicatedVars.add( + new DuplicateVar(field.getType(), dupFieldName(field), field.getName(), true)); + } else { + if (field.isList()) { + sb.append(Utils.indent(listChildCode(field), 2)); + sb.append(System.lineSeparator()); + duplicatedVars.add( + new DuplicateVar(field.getType(), dupFieldName(field), field.getName(), false)); + } else if (field.isOption()) { + sb.append(Utils.indent(optionChildCode(field), 2)); + sb.append(System.lineSeparator()); + duplicatedVars.add( + new DuplicateVar(field.getType(), dupFieldName(field), field.getName(), false)); + } else { + sb.append(Utils.indent(notNullableChildCode(field), 2)); + sb.append(System.lineSeparator()); + duplicatedVars.add( + new DuplicateVar(field.getType(), dupFieldName(field), field.getName(), true)); + } + } + } else { + sb.append(Utils.indent(nonChildCode(field), 2)); + sb.append(System.lineSeparator()); + duplicatedVars.add( + new DuplicateVar(field.getType(), dupFieldName(field), field.getName(), false)); + } + } + + var ctorParams = matchCtorParams(duplicatedVars); + var newSubclass = newSubclass(ctorParams); + sb.append(newSubclass); + + // Rest of the fields that need to be set + var restOfDuplicatedVars = Utils.diff(duplicatedVars, ctorParams); + for (var duplVar : restOfDuplicatedVars) { + sb.append(" ").append("duplicated.").append(duplVar.originalName).append(" = "); + if (duplVar.needsCast) { + sb.append("(").append(duplVar.type).append(") "); + } + sb.append(duplVar.duplicatedName).append(";").append(System.lineSeparator()); + } + + sb.append(" ").append("return duplicated;").append(System.lineSeparator()); + + sb.append("}"); + sb.append(System.lineSeparator()); + var defaultDuplicateMethod = sb.toString(); + return defaultDuplicateMethod + System.lineSeparator() + parameterlessDuplicateMethod(); + } + + private List metaFields() { + var procEnv = ctx.getProcessingEnvironment(); + var diagTypeElem = Utils.diagnosticStorageTypeElement(procEnv); + var metaTypeElem = Utils.metadataStorageTypeElement(procEnv); + var locationTypeElem = Utils.identifiedLocationTypeElement(procEnv); + var uuidTypeElem = Utils.uuidTypeElement(procEnv); + return List.of( + new MetaField(diagTypeElem.asType(), "diagnostics"), + new MetaField(metaTypeElem.asType(), "passData"), + new MetaField(locationTypeElem.asType(), "location"), + new MetaField(uuidTypeElem.asType(), "id")); + } + + private String parameterlessDuplicateMethod() { + var code = + """ + public $retType duplicate() { + return duplicate(true, true, true, false); + } + """ + .replace("$retType", dupMethodRetType()); + return code; + } + + private static String dupFieldName(Field field) { + return field.getName() + "Duplicated"; + } + + private String nullableChildCode(Field nullableChild) { + Utils.hardAssert(nullableChild.isNullable() && nullableChild.isChild()); + return """ + IR $dupName = null; + if ($childName != null) { + $dupName = $childName.duplicate($parameterNames); + if (!($dupName instanceof $childType)) { + throw new IllegalStateException("Duplicated child is not of the expected type: " + $dupName); + } + } + """ + .replace("$childType", nullableChild.getSimpleTypeName()) + .replace("$childName", nullableChild.getName()) + .replace("$dupName", dupFieldName(nullableChild)) + .replace("$parameterNames", String.join(", ", parameterNames())); + } + + private String notNullableChildCode(Field child) { + assert child.isChild() && !child.isNullable() && !child.isList() && !child.isOption(); + return """ + IR $dupName = $childName.duplicate($parameterNames); + if (!($dupName instanceof $childType)) { + throw new IllegalStateException("Duplicated child is not of the expected type: " + $dupName); + } + """ + .replace("$childType", child.getSimpleTypeName()) + .replace("$childName", child.getName()) + .replace("$dupName", dupFieldName(child)) + .replace("$parameterNames", String.join(", ", parameterNames())); + } + + private String listChildCode(Field listChild) { + Utils.hardAssert(listChild.isChild() && listChild.isList()); + return """ + $childListType $dupName = + $childName.map(child -> { + IR dupChild = child.duplicate($parameterNames); + if (!(dupChild instanceof $childType)) { + throw new IllegalStateException("Duplicated child is not of the expected type: " + dupChild); + } + return ($childType) dupChild; + }); + """ + .replace("$childListType", listChild.getSimpleTypeName()) + .replace("$childType", listChild.getTypeParameter().getSimpleName()) + .replace("$childName", listChild.getName()) + .replace("$dupName", dupFieldName(listChild)) + .replace("$parameterNames", String.join(", ", parameterNames())); + } + + private String optionChildCode(Field optionChild) { + Utils.hardAssert(optionChild.isOption() && optionChild.isChild()); + return """ + $childOptType $dupName = $childName; + if ($childName.isDefined()) { + var duplicated = $childName.get().duplicate($parameterNames); + if (!(duplicated instanceof $childType)) { + throw new IllegalStateException("Duplicated child is not of the expected type: " + $dupName); + } + $dupName = Option.apply(duplicated); + } + """ + .replace("$childOptType", optionChild.getSimpleTypeName()) + .replace("$childType", optionChild.getTypeParameter().getSimpleName()) + .replace("$childName", optionChild.getName()) + .replace("$dupName", dupFieldName(optionChild)) + .replace("$parameterNames", String.join(", ", parameterNames())); + } + + private static String nonChildCode(Field field) { + Utils.hardAssert(!field.isChild()); + return """ + $childType $dupName = $childName; + """ + .replace("$childType", field.getSimpleTypeName()) + .replace("$childName", field.getName()) + .replace("$dupName", dupFieldName(field)); + } + + private List parameterNames() { + return parameters.stream().map(Parameter::name).collect(Collectors.toList()); + } + + /** Generate code for call of a constructor of the subclass. */ + private String newSubclass(List ctorParams) { + var subClassType = ctx.getProcessedClass().getClazz().getSimpleName().toString(); + var ctor = ctx.getProcessedClass().getCtor(); + Utils.hardAssert(ctor.getParameters().size() == ctorParams.size()); + var sb = new StringBuilder(); + sb.append(" ") + .append(subClassType) + .append(" duplicated") + .append(" = ") + .append("new ") + .append(subClassType) + .append("("); + var ctorParamsStr = + ctorParams.stream() + .map( + ctorParam -> { + if (ctorParam.needsCast) { + return "(" + ctorParam.type + ") " + ctorParam.duplicatedName; + } else { + return ctorParam.duplicatedName; + } + }) + .collect(Collectors.joining(", ")); + sb.append(ctorParamsStr).append(");").append(System.lineSeparator()); + return sb.toString(); + } + + /** + * Returns sublist of the given list that matches the parameters of the constructor of the + * subclass. + * + * @param duplicatedVars All duplicated variables. + * @return sublist, potentially reordered. + */ + private List matchCtorParams(List duplicatedVars) { + var ctorParams = new ArrayList(); + for (var subclassCtorParam : ctx.getSubclassConstructorParameters()) { + var paramType = subclassCtorParam.getTypeName(); + var paramName = subclassCtorParam.name(); + duplicatedVars.stream() + .filter( + var -> + var.type.equals(subclassCtorParam.getType()) + && var.originalName.equals(paramName)) + .findFirst() + .ifPresentOrElse( + ctorParams::add, + () -> { + var errMsg = + String.format( + "No matching field found for parameter %s of type %s. All duplicated vars:" + + " %s", + paramName, paramType, duplicatedVars); + throw new IRProcessingException(errMsg, ctx.getProcessedClass().getCtor()); + }); + } + return ctorParams; + } + + private String dupMethodRetType() { + return ctx.getProcessedClass().getClazz().getSimpleName().toString(); + } + + /** + * @param duplicatedName Name of the duplicated variable + * @param originalName Name of the original variable (field) + * @param needsCast If the duplicated variable needs to be cast to its type in the return + * statement. + */ + private record DuplicateVar( + TypeMirror type, String duplicatedName, String originalName, boolean needsCast) {} + + /** + * Parameter for the duplicate method + * + * @param type + * @param name + */ + private record Parameter(TypeMirror type, String name) { + + @Override + public String toString() { + return type + " " + name; + } + } + + private record MetaField(TypeMirror type, String name) {} +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/EqualsMethodGenerator.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/EqualsMethodGenerator.java new file mode 100644 index 000000000000..84960e2923de --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/EqualsMethodGenerator.java @@ -0,0 +1,37 @@ +package org.enso.runtime.parser.processor.methodgen; + +import org.enso.runtime.parser.processor.GeneratedClassContext; + +public final class EqualsMethodGenerator { + private final GeneratedClassContext ctx; + + public EqualsMethodGenerator(GeneratedClassContext ctx) { + this.ctx = ctx; + } + + public String generateMethodCode() { + var sb = new StringBuilder(); + sb.append("@Override").append(System.lineSeparator()); + sb.append("public boolean equals(Object o) {").append(System.lineSeparator()); + sb.append(" if (this == o) {").append(System.lineSeparator()); + sb.append(" return true;").append(System.lineSeparator()); + sb.append(" }").append(System.lineSeparator()); + sb.append(" if (o instanceof ") + .append(ctx.getClassName()) + .append(" other) {") + .append(System.lineSeparator()); + for (var field : ctx.getAllFields()) { + sb.append( + " if (!(Objects.equals(this.$name, other.$name))) {" + .replace("$name", field.name())) + .append(System.lineSeparator()); + sb.append(" return false;").append(System.lineSeparator()); + sb.append(" }").append(System.lineSeparator()); + } + sb.append(" return true;").append(System.lineSeparator()); + sb.append(" }").append(System.lineSeparator()); + sb.append(" return false;").append(System.lineSeparator()); + sb.append("}").append(System.lineSeparator()); + return sb.toString(); + } +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/HashCodeMethodGenerator.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/HashCodeMethodGenerator.java new file mode 100644 index 000000000000..bdc5da20cce0 --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/HashCodeMethodGenerator.java @@ -0,0 +1,27 @@ +package org.enso.runtime.parser.processor.methodgen; + +import java.util.stream.Collectors; +import org.enso.runtime.parser.processor.ClassField; +import org.enso.runtime.parser.processor.GeneratedClassContext; + +public final class HashCodeMethodGenerator { + private final GeneratedClassContext ctx; + + public HashCodeMethodGenerator(GeneratedClassContext ctx) { + this.ctx = ctx; + } + + public String generateMethodCode() { + var fieldList = + ctx.getAllFields().stream().map(ClassField::name).collect(Collectors.joining(", ")); + var code = + """ + @Override + public int hashCode() { + return Objects.hash($fieldList); + } + """ + .replace("$fieldList", fieldList); + return code; + } +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/MapExpressionsMethodGenerator.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/MapExpressionsMethodGenerator.java new file mode 100644 index 000000000000..c7e25e91d69d --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/MapExpressionsMethodGenerator.java @@ -0,0 +1,229 @@ +package org.enso.runtime.parser.processor.methodgen; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import javax.lang.model.element.ExecutableElement; +import org.enso.runtime.parser.processor.ClassField; +import org.enso.runtime.parser.processor.GeneratedClassContext; +import org.enso.runtime.parser.processor.IRProcessingException; +import org.enso.runtime.parser.processor.field.Field; +import org.enso.runtime.parser.processor.utils.Utils; + +public final class MapExpressionsMethodGenerator { + private final ExecutableElement mapExpressionsMethod; + private final GeneratedClassContext ctx; + private static final String METHOD_NAME = "mapExpressions"; + + /** + * @param mapExpressionsMethod Reference to {@code mapExpressions} method in the interface for + * which the class is generated. + * @param ctx + */ + public MapExpressionsMethodGenerator( + ExecutableElement mapExpressionsMethod, GeneratedClassContext ctx) { + ensureMapExpressionsMethodHasExpectedSignature(mapExpressionsMethod); + this.mapExpressionsMethod = mapExpressionsMethod; + this.ctx = Objects.requireNonNull(ctx); + } + + private void ensureMapExpressionsMethodHasExpectedSignature( + ExecutableElement mapExpressionsMethod) { + var parameters = mapExpressionsMethod.getParameters(); + if (parameters.size() != 1) { + throw new IRProcessingException( + "Map expressions method must have 1 parameter", mapExpressionsMethod); + } + } + + public String generateMapExpressionsMethodCode() { + var sb = new StringBuilder(); + var subclassType = ctx.getProcessedClass().getClazz().getSimpleName().toString(); + sb.append("@Override").append(System.lineSeparator()); + sb.append("public ") + .append(subclassType) + .append(" ") + .append(METHOD_NAME) + .append("(") + .append("Function fn") + .append(") {") + .append(System.lineSeparator()); + + var children = ctx.getUserFields().stream().filter(Field::isChild); + // A list of new children that are created by calling mapExpressions on the existing children + // Or the function directly if the child is of Expression type (this prevents + // recursion). + var newChildren = + children + .map( + child -> { + ExecutableElement childsMapExprMethod; + if (child.isList() || child.isOption()) { + childsMapExprMethod = + Utils.findMapExpressionsMethod( + child.getTypeParameter(), ctx.getProcessingEnvironment()); + } else { + var childTypeElem = Utils.typeMirrorToElement(child.getType()); + childsMapExprMethod = + Utils.findMapExpressionsMethod( + childTypeElem, ctx.getProcessingEnvironment()); + } + + var typeUtils = ctx.getProcessingEnvironment().getTypeUtils(); + var childsMapExprMethodRetType = + typeUtils.asElement(childsMapExprMethod.getReturnType()); + var shouldCast = + !typeUtils.isSameType(child.getType(), childsMapExprMethodRetType.asType()); + if (child.isList() || child.isOption()) { + shouldCast = false; + } + + String newChildType = childsMapExprMethodRetType.getSimpleName().toString(); + if (child.isList()) { + newChildType = "List<" + newChildType + ">"; + } else if (child.isOption()) { + newChildType = "Option<" + newChildType + ">"; + } + var childIsExpression = + Utils.isExpression( + childsMapExprMethodRetType, ctx.getProcessingEnvironment()); + + var newChildName = child.getName() + "Mapped"; + sb.append(" ").append(newChildType).append(" ").append(newChildName); + if (child.isNullable()) { + sb.append(" = null;").append(System.lineSeparator()); + sb.append(" if (") + .append(child.getName()) + .append(" != null) {") + .append(System.lineSeparator()); + if (childIsExpression) { + // childMapped = fn.apply(child); + sb.append(" ") + .append(newChildName) + .append(" = fn.apply(") + .append(child.getName()) + .append(");") + .append(System.lineSeparator()); + } else { + // childMapped = child.mapExpressions(fn); + sb.append(" ") + .append(newChildName) + .append(".") + .append(METHOD_NAME) + .append("(fn);") + .append(System.lineSeparator()); + } + sb.append(" }").append(System.lineSeparator()); + } else { + if (!child.isList() && !child.isOption()) { + if (childIsExpression) { + // ChildType childMapped = fn.apply(child); + sb.append(" = ") + .append("fn.apply(") + .append(child.getName()) + .append(");") + .append(System.lineSeparator()); + } else { + // ChildType childMapped = child.mapExpressions(fn); + sb.append(" = ") + .append(child.getName()) + .append(".") + .append(METHOD_NAME) + .append("(fn);") + .append(System.lineSeparator()); + } + } else { + Utils.hardAssert(child.isList() || child.isOption()); + // List childMapped = child.map(e -> e.mapExpressions(fn)); + sb.append(" = ").append(child.getName()).append(".map(e -> "); + if (childIsExpression) { + // List childMapped = child.map(e -> fn.apply(e)); + sb.append("fn.apply(e)"); + } else { + // List childMapped = child.map(e -> e.mapExpressions(fn)); + sb.append("e.").append(METHOD_NAME).append("(fn)"); + } + sb.append(");").append(System.lineSeparator()); + } + } + return new MappedChild(newChildName, child, shouldCast); + }) + .toList(); + if (newChildren.isEmpty()) { + sb.append(" return ") + .append("(") + .append(ctx.getProcessedClass().getClazz().getSimpleName().toString()) + .append(") this;") + .append(System.lineSeparator()); + sb.append("}").append(System.lineSeparator()); + return sb.toString(); + } + sb.append(" // Only copy if some of the children actually changed") + .append(System.lineSeparator()); + var changedCond = + newChildren.stream() + .map(newChild -> newChild.newChildName + " != " + newChild.child.getName()) + .collect(Collectors.joining(" || ")); + sb.append(" ").append("if (").append(changedCond).append(") {").append(System.lineSeparator()); + sb.append(" ").append("var bldr = new Builder();").append(System.lineSeparator()); + for (MappedChild newChild : newChildren) { + if (newChild.shouldCast) { + sb.append(" ") + .append("if (!(") + .append(newChild.newChildName) + .append(" instanceof ") + .append(newChild.child.getSimpleTypeName()) + .append(")) {") + .append(System.lineSeparator()); + sb.append(" ") + .append( + "throw new IllegalStateException(\"Duplicated child is not of the expected" + + " type: \" + ") + .append(newChild.newChildName) + .append(");") + .append(System.lineSeparator()); + sb.append(" }").append(System.lineSeparator()); + } + sb.append(" ").append("bldr.").append(newChild.child.getName()).append("("); + if (newChild.shouldCast) { + sb.append("(").append(newChild.child.getSimpleTypeName()).append(") "); + } + sb.append(newChild.newChildName).append(");").append(System.lineSeparator()); + } + for (var field : restOfTheFields(newChildren)) { + sb.append(" ") + .append("bldr.") + .append(field.name()) + .append("(") + .append(field.name()) + .append(");") + .append(System.lineSeparator()); + } + sb.append(" return bldr.build();").append(System.lineSeparator()); + sb.append(" } else { ").append(System.lineSeparator()); + sb.append(" // None of the mapped children changed - just return this") + .append(System.lineSeparator()); + sb.append(" return ") + .append("(") + .append(ctx.getProcessedClass().getClazz().getSimpleName().toString()) + .append(") this;") + .append(System.lineSeparator()); + sb.append(" }").append(System.lineSeparator()); + sb.append("}").append(System.lineSeparator()); + return sb.toString(); + } + + private List restOfTheFields(List newChildren) { + var restOfFields = new ArrayList(); + for (var field : ctx.getAllFields()) { + if (newChildren.stream() + .noneMatch(newChild -> newChild.child.getName().equals(field.name()))) { + restOfFields.add(field); + } + } + return restOfFields; + } + + private record MappedChild(String newChildName, Field child, boolean shouldCast) {} +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/SetLocationMethodGenerator.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/SetLocationMethodGenerator.java new file mode 100644 index 000000000000..39900dd180fd --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/SetLocationMethodGenerator.java @@ -0,0 +1,51 @@ +package org.enso.runtime.parser.processor.methodgen; + +import javax.lang.model.element.ExecutableElement; +import org.enso.runtime.parser.processor.GeneratedClassContext; +import org.enso.runtime.parser.processor.IRProcessingException; + +public class SetLocationMethodGenerator { + private final ExecutableElement setLocationMethod; + private final GeneratedClassContext ctx; + + public SetLocationMethodGenerator( + ExecutableElement setLocationMethod, GeneratedClassContext ctx) { + ensureCorrectSignature(setLocationMethod); + this.ctx = ctx; + this.setLocationMethod = setLocationMethod; + } + + private static void ensureCorrectSignature(ExecutableElement setLocationMethod) { + if (!setLocationMethod.getSimpleName().toString().equals("setLocation")) { + throw new IRProcessingException( + "setLocation method must be named setLocation, but was: " + setLocationMethod, + setLocationMethod); + } + if (setLocationMethod.getParameters().size() != 1) { + throw new IRProcessingException( + "setLocation method must have exactly one parameter, but had: " + + setLocationMethod.getParameters(), + setLocationMethod); + } + } + + public String generateMethodCode() { + var code = + """ + @Override + public $retType setLocation(Option location) { + IdentifiedLocation loc = null; + if (location.isDefined()) { + loc = location.get(); + } + return builder().location(loc).build(); + } + """ + .replace("$retType", retType()); + return code; + } + + private String retType() { + return ctx.getProcessedClass().getClazz().getSimpleName().toString(); + } +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/ToStringMethodGenerator.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/ToStringMethodGenerator.java new file mode 100644 index 000000000000..b0bae908d3a3 --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/ToStringMethodGenerator.java @@ -0,0 +1,54 @@ +package org.enso.runtime.parser.processor.methodgen; + +import java.util.stream.Collectors; +import javax.lang.model.element.ElementKind; +import org.enso.runtime.parser.processor.GeneratedClassContext; + +public class ToStringMethodGenerator { + private final GeneratedClassContext ctx; + + public ToStringMethodGenerator(GeneratedClassContext ctx) { + this.ctx = ctx; + } + + public String generateMethodCode() { + var docs = + """ + /** + * Returns a one-line string representation of this IR object. + */ + """; + var sb = new StringBuilder(); + sb.append(docs); + sb.append("@Override").append(System.lineSeparator()); + sb.append("public String toString() {").append(System.lineSeparator()); + sb.append(" String ret = ").append(System.lineSeparator()); + sb.append(" ").append(quoted(className())).append(System.lineSeparator()); + sb.append(" + ").append(quoted("(")).append(System.lineSeparator()); + var fieldsStrRepr = + ctx.getAllFields().stream() + .map(field -> " \"$fieldName = \" + $fieldName".replace("$fieldName", field.name())) + .collect(Collectors.joining(" + \", \" + " + System.lineSeparator())); + sb.append(" + ").append(fieldsStrRepr).append(System.lineSeparator()); + sb.append(" + ").append(quoted(")")).append(";").append(System.lineSeparator()); + sb.append(" return ret.replaceAll(System.lineSeparator(), \" \");") + .append(System.lineSeparator()); + sb.append("}").append(System.lineSeparator()); + return sb.toString(); + } + + private String className() { + var clazz = ctx.getProcessedClass().getClazz(); + var enclosingElem = clazz.getEnclosingElement(); + if (enclosingElem.getKind() == ElementKind.INTERFACE + || enclosingElem.getKind() == ElementKind.CLASS) { + return enclosingElem.getSimpleName().toString() + "." + clazz.getSimpleName().toString(); + } else { + return clazz.getSimpleName().toString(); + } + } + + private static String quoted(String str) { + return '"' + str + '"'; + } +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/utils/InterfaceHierarchyVisitor.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/utils/InterfaceHierarchyVisitor.java new file mode 100644 index 000000000000..89ebed243817 --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/utils/InterfaceHierarchyVisitor.java @@ -0,0 +1,21 @@ +package org.enso.runtime.parser.processor.utils; + +import javax.lang.model.element.TypeElement; + +/** + * A visitor for traversing the interface hierarchy of an interface - it iterates over all the super + * interfaces until it encounters {@code org.enso.compiler.ir.IR} interface. The iteration can be + * stopped by returning a non-null value from the visitor. Follows a similar pattern as {@link + * com.oracle.truffle.api.frame.FrameInstanceVisitor}. + */ +@FunctionalInterface +public interface InterfaceHierarchyVisitor { + /** + * Visits the interface hierarchy of the given interface. + * + * @param iface the interface to visit + * @return If not-null, the iteration is stopped and the value is returned. If null, the iteration + * continues. + */ + T visitInterface(TypeElement iface); +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/utils/Utils.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/utils/Utils.java new file mode 100644 index 000000000000..7dca953cde38 --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/utils/Utils.java @@ -0,0 +1,308 @@ +package org.enso.runtime.parser.processor.utils; + +import java.lang.annotation.Annotation; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import org.enso.runtime.parser.processor.IRProcessingException; + +public final class Utils { + + private static final String MAP_EXPRESSIONS = "mapExpressions"; + private static final String DUPLICATE = "duplicate"; + private static final String IR_INTERFACE_SIMPLE_NAME = "IR"; + private static final String IR_INTERFACE_FQN = "org.enso.compiler.core.IR"; + private static final String EXPRESSION_FQN = "org.enso.compiler.core.ir.Expression"; + private static final String SCALA_LIST = "scala.collection.immutable.List"; + private static final String SCALA_OPTION = "scala.Option"; + private static final String DIAGNOSTIC_STORAGE_FQN = + "org.enso.compiler.core.ir.DiagnosticStorage"; + private static final String IDENTIFIED_LOCATION_FQN = + "org.enso.compiler.core.ir.IdentifiedLocation"; + private static final String METADATA_STORAGE_FQN = "org.enso.compiler.core.ir.MetadataStorage"; + private static final String UUID_FQN = "java.util.UUID"; + + private Utils() {} + + /** Returns true if the given {@code type} is a subtype of {@code org.enso.compiler.core.IR}. */ + public static boolean isSubtypeOfIR(TypeElement type, ProcessingEnvironment processingEnv) { + var irIfaceFound = + iterateSuperInterfaces( + type, + processingEnv, + (TypeElement iface) -> { + // current.getQualifiedName().toString() returns only "IR" as well, so we can't use + // it. + // This is because runtime-parser-processor project does not depend on runtime-parser + // and + // so the org.enso.compiler.core.IR interface is not available in the classpath. + if (iface.getSimpleName().toString().equals(IR_INTERFACE_SIMPLE_NAME)) { + return true; + } + return null; + }); + return irIfaceFound != null; + } + + /** Returns true if the given {@code type} is an {@code org.enso.compiler.core.IR} interface. */ + public static boolean isIRInterface(TypeMirror type, ProcessingEnvironment processingEnv) { + var elem = processingEnv.getTypeUtils().asElement(type); + return elem.getKind() == ElementKind.INTERFACE + && elem.getSimpleName().toString().equals(IR_INTERFACE_SIMPLE_NAME); + } + + public static TypeElement irTypeElement(ProcessingEnvironment procEnv) { + var ret = procEnv.getElementUtils().getTypeElement(IR_INTERFACE_FQN); + hardAssert(ret != null); + return ret; + } + + public static TypeElement diagnosticStorageTypeElement(ProcessingEnvironment procEnv) { + var ret = procEnv.getElementUtils().getTypeElement(DIAGNOSTIC_STORAGE_FQN); + hardAssert(ret != null); + return ret; + } + + public static TypeElement identifiedLocationTypeElement(ProcessingEnvironment procEnv) { + var ret = procEnv.getElementUtils().getTypeElement(IDENTIFIED_LOCATION_FQN); + hardAssert(ret != null); + return ret; + } + + public static TypeElement metadataStorageTypeElement(ProcessingEnvironment procEnv) { + var ret = procEnv.getElementUtils().getTypeElement(METADATA_STORAGE_FQN); + hardAssert(ret != null); + return ret; + } + + public static TypeElement uuidTypeElement(ProcessingEnvironment procEnv) { + var ret = procEnv.getElementUtils().getTypeElement(UUID_FQN); + hardAssert(ret != null); + return ret; + } + + public static boolean isExpression(Element elem, ProcessingEnvironment processingEnvironment) { + if (elem instanceof TypeElement typeElem) { + var exprType = expressionType(processingEnvironment); + return processingEnvironment.getTypeUtils().isSameType(typeElem.asType(), exprType.asType()); + } + return false; + } + + /** Returns true if the given type extends {@code org.enso.compiler.core.ir.Expression} */ + public static boolean isSubtypeOfExpression( + TypeMirror type, ProcessingEnvironment processingEnv) { + var exprType = expressionType(processingEnv).asType(); + return processingEnv.getTypeUtils().isAssignable(type, exprType); + } + + public static TypeElement expressionType(ProcessingEnvironment procEnv) { + return procEnv.getElementUtils().getTypeElement(EXPRESSION_FQN); + } + + /** Converts all the FQN parts of the type name to simple names. Includes type arguments. */ + public static String simpleTypeName(TypeMirror typeMirror) { + if (typeMirror.getKind() == TypeKind.DECLARED) { + var declared = (DeclaredType) typeMirror; + var typeArgs = declared.getTypeArguments(); + var typeElem = (TypeElement) declared.asElement(); + if (!typeArgs.isEmpty()) { + var typeArgsStr = + typeArgs.stream().map(Utils::simpleTypeName).collect(Collectors.joining(", ")); + return typeElem.getSimpleName().toString() + "<" + typeArgsStr + ">"; + } else { + return typeElem.getSimpleName().toString(); + } + } + return typeMirror.toString(); + } + + /** + * Returns (a possibly empty) list of FQN that should be imported in order to use the given {@code + * typeMirror}. + * + * @return List of FQN, intended to be used in import statements. + */ + public static List getImportedTypes(TypeMirror typeMirror) { + var importedTypes = new ArrayList(); + if (typeMirror.getKind() == TypeKind.DECLARED) { + var declared = (DeclaredType) typeMirror; + var typeElem = (TypeElement) declared.asElement(); + var typeArgs = declared.getTypeArguments(); + importedTypes.add(typeElem.getQualifiedName().toString()); + for (var typeArg : typeArgs) { + importedTypes.addAll(getImportedTypes(typeArg)); + } + } + return importedTypes; + } + + public static String indent(String code, int indentation) { + return code.lines() + .map(line -> " ".repeat(indentation) + line) + .collect(Collectors.joining(System.lineSeparator())); + } + + /** + * Returns null if the given {@code typeMirror} is not a declared type and thus has no associated + * {@link TypeElement}. + */ + public static TypeElement typeMirrorToElement(TypeMirror typeMirror) { + if (typeMirror.getKind() == TypeKind.DECLARED) { + var elem = ((DeclaredType) typeMirror).asElement(); + if (elem instanceof TypeElement typeElem) { + return typeElem; + } + } + return null; + } + + public static boolean isScalaOption(TypeMirror type, ProcessingEnvironment procEnv) { + var elem = procEnv.getTypeUtils().asElement(type); + if (elem instanceof TypeElement typeElem) { + var optionType = procEnv.getElementUtils().getTypeElement(SCALA_OPTION); + return procEnv.getTypeUtils().isSameType(optionType.asType(), typeElem.asType()); + } + return false; + } + + public static boolean isScalaList(TypeMirror type, ProcessingEnvironment procEnv) { + var elem = procEnv.getTypeUtils().asElement(type); + if (elem instanceof TypeElement typeElem) { + var listType = procEnv.getElementUtils().getTypeElement(SCALA_LIST); + return procEnv.getTypeUtils().isSameType(listType.asType(), typeElem.asType()); + } + return false; + } + + /** + * Finds a method in the interface hierarchy. The interface hierarchy processing starts from + * {@code interfaceType} and iterates until {@code org.enso.compiler.core.IR} interface type is + * encountered. Every method in the hierarchy is checked by {@code methodPredicate}. + * + * @param interfaceType Type of the interface. Must extend {@code org.enso.compiler.core.IR}. + * @param procEnv + * @param methodPredicate Predicate that is called for each method in the hierarchy. + * @return Method that satisfies the predicate or null if no such method is found. + */ + public static ExecutableElement findMethod( + TypeElement interfaceType, + ProcessingEnvironment procEnv, + Predicate methodPredicate) { + var foundMethod = + iterateSuperInterfaces( + interfaceType, + procEnv, + (TypeElement superInterface) -> { + for (var enclosedElem : superInterface.getEnclosedElements()) { + if (enclosedElem instanceof ExecutableElement execElem) { + if (methodPredicate.test(execElem)) { + return execElem; + } + } + } + return null; + }); + return foundMethod; + } + + /** + * Find any override of {@link org.enso.compiler.core.IR#duplicate(boolean, boolean, boolean, + * boolean) duplicate method}. Or the duplicate method on the interface itself. Note that there + * can be an override with a different return type in a sub interface. + * + * @param interfaceType Interface from where the search is started. All super interfaces are + * searched transitively. + * @return not null. + */ + public static ExecutableElement findDuplicateMethod( + TypeElement interfaceType, ProcessingEnvironment procEnv) { + var duplicateMethod = findMethod(interfaceType, procEnv, Utils::isDuplicateMethod); + hardAssert( + duplicateMethod != null, + "Interface " + + interfaceType.getQualifiedName() + + " must implement IR, so it must declare duplicate method"); + return duplicateMethod; + } + + public static ExecutableElement findMapExpressionsMethod( + TypeElement interfaceType, ProcessingEnvironment processingEnv) { + var mapExprsMethod = + findMethod( + interfaceType, + processingEnv, + method -> method.getSimpleName().toString().equals(MAP_EXPRESSIONS)); + hardAssert( + mapExprsMethod != null, + "mapExpressions method must be found it must be defined at least on IR super interface"); + return mapExprsMethod; + } + + public static void hardAssert(boolean condition) { + hardAssert(condition, "Assertion failed"); + } + + public static void hardAssert(boolean condition, String msg) { + if (!condition) { + throw new IRProcessingException(msg, null); + } + } + + public static boolean hasNoAnnotations(Element element) { + return element.getAnnotationMirrors().isEmpty(); + } + + public static boolean hasAnnotation( + Element element, Class annotationClass) { + return element.getAnnotation(annotationClass) != null; + } + + private static boolean isDuplicateMethod(ExecutableElement executableElement) { + return executableElement.getSimpleName().toString().equals(DUPLICATE) + && executableElement.getParameters().size() == 4; + } + + /** + * @param type Type from which the iterations starts. + * @param processingEnv + * @param ifaceVisitor Visitor that is called for each interface. + * @param + */ + public static T iterateSuperInterfaces( + TypeElement type, + ProcessingEnvironment processingEnv, + InterfaceHierarchyVisitor ifaceVisitor) { + var interfacesToProcess = new ArrayDeque(); + interfacesToProcess.add(type); + while (!interfacesToProcess.isEmpty()) { + var current = interfacesToProcess.pop(); + var iterationResult = ifaceVisitor.visitInterface(current); + if (iterationResult != null) { + return iterationResult; + } + // Add all super interfaces to the queue + for (var superInterface : current.getInterfaces()) { + var superInterfaceElem = processingEnv.getTypeUtils().asElement(superInterface); + if (superInterfaceElem instanceof TypeElement superInterfaceTypeElem) { + interfacesToProcess.add(superInterfaceTypeElem); + } + } + } + return null; + } + + public static List diff(List superset, List subset) { + return superset.stream().filter(e -> !subset.contains(e)).collect(Collectors.toList()); + } +} diff --git a/engine/runtime-parser/src/main/java/module-info.java b/engine/runtime-parser/src/main/java/module-info.java index 79de84d6dc8f..563b0ae6ada2 100644 --- a/engine/runtime-parser/src/main/java/module-info.java +++ b/engine/runtime-parser/src/main/java/module-info.java @@ -2,6 +2,8 @@ requires org.enso.syntax; requires scala.library; requires org.enso.persistance; + requires static org.enso.runtime.parser.dsl; + requires static org.enso.runtime.parser.processor; exports org.enso.compiler.core; exports org.enso.compiler.core.ir; diff --git a/engine/runtime-parser/src/main/java/org/enso/compiler/core/ir/CallArgument.java b/engine/runtime-parser/src/main/java/org/enso/compiler/core/ir/CallArgument.java new file mode 100644 index 000000000000..8d65af13cbc6 --- /dev/null +++ b/engine/runtime-parser/src/main/java/org/enso/compiler/core/ir/CallArgument.java @@ -0,0 +1,84 @@ +package org.enso.compiler.core.ir; + +import java.util.function.Function; +import org.enso.compiler.core.IR; +import org.enso.runtime.parser.dsl.GenerateFields; +import org.enso.runtime.parser.dsl.GenerateIR; +import org.enso.runtime.parser.dsl.IRChild; +import org.enso.runtime.parser.dsl.IRField; +import scala.Option; + +/** Call-site arguments in Enso. */ +public interface CallArgument extends IR { + /** The name of the argument, if present. */ + Option name(); + + /** The expression of the argument, if present. */ + Expression value(); + + /** Flag indicating that the argument was generated by compiler. */ + boolean isSynthetic(); + + @Override + CallArgument mapExpressions(Function fn); + + @Override + CallArgument duplicate( + boolean keepLocations, + boolean keepMetadata, + boolean keepDiagnostics, + boolean keepIdentifiers); + + /** + * A representation of an argument at a function call site. + * + *

A {@link CallArgument} where the {@link CallArgument#value()} is an {@link Name.Blank} is a + * representation of a lambda shorthand argument. + */ + @GenerateIR(interfaces = {CallArgument.class}) + final class Specified extends SpecifiedGen { + + /** + * @param name the name of the argument being called, if present + * @param value the expression being passed as the argument's value + * @param isSynthetic the flag indicating that the argument was generated by compiler + */ + @GenerateFields + public Specified( + @IRChild Option name, + @IRChild Expression value, + @IRField boolean isSynthetic, + IdentifiedLocation identifiedLocation, + MetadataStorage passData) { + super(name, value, isSynthetic, identifiedLocation, passData); + } + + public Specified( + Option name, + Expression value, + boolean isSynthetic, + IdentifiedLocation identifiedLocation) { + this(name, value, isSynthetic, identifiedLocation, new MetadataStorage()); + } + + public Specified copy(Expression value) { + return copy( + this.diagnostics, + this.passData, + this.location, + this.id, + this.name(), + value, + this.isSynthetic()); + } + + @Override + public String showCode(int indent) { + if (name().isDefined()) { + return "(" + name().get().showCode(indent) + " = " + value().showCode(indent) + ")"; + } else { + return value().showCode(indent); + } + } + } +} diff --git a/engine/runtime-parser/src/main/java/org/enso/compiler/core/ir/Empty.java b/engine/runtime-parser/src/main/java/org/enso/compiler/core/ir/Empty.java new file mode 100644 index 000000000000..46a833939618 --- /dev/null +++ b/engine/runtime-parser/src/main/java/org/enso/compiler/core/ir/Empty.java @@ -0,0 +1,16 @@ +package org.enso.compiler.core.ir; + +import org.enso.runtime.parser.dsl.GenerateFields; +import org.enso.runtime.parser.dsl.GenerateIR; + +@GenerateIR(interfaces = {Expression.class}) +public final class Empty extends EmptyGen { + @GenerateFields + public Empty(IdentifiedLocation identifiedLocation, MetadataStorage passData) { + super(identifiedLocation, passData); + } + + public Empty(IdentifiedLocation identifiedLocation) { + this(identifiedLocation, new MetadataStorage()); + } +} diff --git a/engine/runtime-parser/src/main/scala/org/enso/compiler/core/ir/CallArgument.scala b/engine/runtime-parser/src/main/scala/org/enso/compiler/core/ir/CallArgument.scala deleted file mode 100644 index 87feed0d1717..000000000000 --- a/engine/runtime-parser/src/main/scala/org/enso/compiler/core/ir/CallArgument.scala +++ /dev/null @@ -1,161 +0,0 @@ -package org.enso.compiler.core.ir - -import org.enso.compiler.core.{IR, Identifier} -import org.enso.compiler.core.Implicits.{ShowPassData, ToStringHelper} - -import java.util.UUID - -/** Call-site arguments in Enso. */ -sealed trait CallArgument extends IR { - - /** The name of the argument, if present. */ - def name: Option[Name] - - /** The expression of the argument, if present. */ - def value: Expression - - /** Flag indicating that the argument was generated by compiler. */ - def isSynthetic: Boolean - - /** @inheritdoc */ - override def mapExpressions( - fn: java.util.function.Function[Expression, Expression] - ): CallArgument - - /** @inheritdoc */ - override def duplicate( - keepLocations: Boolean = true, - keepMetadata: Boolean = true, - keepDiagnostics: Boolean = true, - keepIdentifiers: Boolean = false - ): CallArgument -} - -object CallArgument { - - /** A representation of an argument at a function call site. - * - * A [[CallArgument]] where the `value` is an [[Name.Blank]] is a - * representation of a lambda shorthand argument. - * - * @param name the name of the argument being called, if present - * @param value the expression being passed as the argument's value - * @param isSynthetic the flag indicating that the argument was generated by compiler - * @param identifiedLocation the source location that the node corresponds to - * @param passData the pass metadata associated with this node - */ - sealed case class Specified( - override val name: Option[Name], - override val value: Expression, - override val isSynthetic: Boolean, - identifiedLocation: IdentifiedLocation, - passData: MetadataStorage = new MetadataStorage() - ) extends CallArgument - with IRKind.Primitive - with LazyDiagnosticStorage - with LazyId { - - /** Creates a copy of `this`. - * - * @param name the name of the argument being called, if present - * @param value the expression being passed as the argument's value - * @param isSynthetic the flag indicating that the argument was generated by compiler - * @param location the source location that the node corresponds to - * @param passData the pass metadata associated with this node - * @param diagnostics compiler diagnostics for this node - * @param id the identifier for the new node - * @return a copy of `this`, updated with the specified values - */ - def copy( - name: Option[Name] = name, - value: Expression = value, - isSynthetic: Boolean = isSynthetic, - location: Option[IdentifiedLocation] = location, - passData: MetadataStorage = passData, - diagnostics: DiagnosticStorage = diagnostics, - id: UUID @Identifier = id - ): Specified = { - if ( - name != this.name - || value != this.value - || location != this.location - || (passData ne this.passData) - || diagnostics != this.diagnostics - || id != this.id - ) { - val res = - new Specified(name, value, isSynthetic, location.orNull, passData) - res.diagnostics = diagnostics - res.id = id - res - } else this - } - - /** @inheritdoc */ - override def duplicate( - keepLocations: Boolean = true, - keepMetadata: Boolean = true, - keepDiagnostics: Boolean = true, - keepIdentifiers: Boolean = false - ): Specified = - copy( - name = name.map( - _.duplicate( - keepLocations, - keepMetadata, - keepDiagnostics, - keepIdentifiers - ) - ), - value = value.duplicate( - keepLocations, - keepMetadata, - keepDiagnostics, - keepIdentifiers - ), - location = if (keepLocations) location else None, - passData = - if (keepMetadata) passData.duplicate else new MetadataStorage(), - diagnostics = if (keepDiagnostics) diagnosticsCopy else null, - id = if (keepIdentifiers) id else null - ) - - /** @inheritdoc */ - override def setLocation( - location: Option[IdentifiedLocation] - ): Specified = copy(location = location) - - /** @inheritdoc */ - override def mapExpressions( - fn: java.util.function.Function[Expression, Expression] - ): Specified = { - copy(name = name.map(n => n.mapExpressions(fn)), value = fn(value)) - } - - /** String representation. */ - override def toString: String = - s""" - |CallArgument.Specified( - |name = $name, - |value = $value, - |location = $location, - |passData = ${this.showPassData}, - |diagnostics = $diagnostics, - |id = $id - |) - |""".toSingleLine - - /** @inheritdoc */ - override def children: List[IR] = - name.map(List(_, value)).getOrElse(List(value)) - - /** @inheritdoc */ - override def showCode(indent: Int): String = { - if (name.isDefined) { - s"(${name.get.showCode(indent)} = ${value.showCode(indent)})" - } else { - s"${value.showCode(indent)}" - } - } - } -} diff --git a/engine/runtime-parser/src/main/scala/org/enso/compiler/core/ir/DiagnosticStorage.scala b/engine/runtime-parser/src/main/scala/org/enso/compiler/core/ir/DiagnosticStorage.scala index f2d87d63c996..b4d02cf58017 100644 --- a/engine/runtime-parser/src/main/scala/org/enso/compiler/core/ir/DiagnosticStorage.scala +++ b/engine/runtime-parser/src/main/scala/org/enso/compiler/core/ir/DiagnosticStorage.scala @@ -61,3 +61,7 @@ final class DiagnosticStorage(initDiagnostics: Seq[Diagnostic] = Seq()) new DiagnosticStorage(this.diagnostics) } } + +object DiagnosticStorage { + def createEmpty(): DiagnosticStorage = new DiagnosticStorage() +} diff --git a/engine/runtime-parser/src/main/scala/org/enso/compiler/core/ir/Empty.scala b/engine/runtime-parser/src/main/scala/org/enso/compiler/core/ir/Empty.scala deleted file mode 100644 index 849d58cd9401..000000000000 --- a/engine/runtime-parser/src/main/scala/org/enso/compiler/core/ir/Empty.scala +++ /dev/null @@ -1,89 +0,0 @@ -package org.enso.compiler.core.ir - -import org.enso.compiler.core.Implicits.{ShowPassData, ToStringHelper} -import org.enso.compiler.core.{IR, Identifier} - -import java.util.UUID - -/** A node representing an empty IR construct that can be used in any place. - * - * @param identifiedLocation the source location that the node corresponds to - * @param passData the pass metadata associated with this node - */ -sealed case class Empty( - override val identifiedLocation: IdentifiedLocation, - override val passData: MetadataStorage = new MetadataStorage() -) extends IR - with Expression - with IRKind.Primitive - with LazyDiagnosticStorage - with LazyId { - - /** Creates a copy of `this` - * - * @param location the source location that the node corresponds to - * @param passData the pass metadata associated with this node - * @param diagnostics compiler diagnostics for this node - * @param id the identifier for the new node - * @return a copy of `this` with the specified fields updated - */ - def copy( - location: Option[IdentifiedLocation] = location, - passData: MetadataStorage = passData, - diagnostics: DiagnosticStorage = diagnostics, - id: UUID @Identifier = id - ): Empty = { - if ( - location != this.location - || (passData ne this.passData) - || diagnostics != this.diagnostics - || id != this.id - ) { - val res = Empty(location.orNull, passData) - res.diagnostics = diagnostics - res.id = id - res - } else this - } - - /** @inheritdoc */ - override def duplicate( - keepLocations: Boolean = true, - keepMetadata: Boolean = true, - keepDiagnostics: Boolean = true, - keepIdentifiers: Boolean = false - ): Empty = - copy( - location = if (keepLocations) location else None, - passData = - if (keepMetadata) passData.duplicate else new MetadataStorage(), - diagnostics = if (keepDiagnostics) diagnosticsCopy else null, - id = if (keepIdentifiers) id else null - ) - - /** @inheritdoc */ - override def setLocation(location: Option[IdentifiedLocation]): Empty = - copy(location = location) - - /** @inheritdoc */ - override def mapExpressions( - fn: java.util.function.Function[Expression, Expression] - ): Empty = this - - /** String representation. */ - override def toString: String = - s""" - |Empty( - |location = $location, - |passData = ${this.showPassData}, - |diagnostics = $diagnostics, - |id = $id - |) - |""".toSingleLine - - /** @inheritdoc */ - override def children: List[IR] = List() - - /** @inheritdoc */ - override def showCode(indent: Int): String = "IR.Empty" -} diff --git a/engine/runtime-parser/src/test/java/org/enso/compiler/core/ParserDependenciesTest.java b/engine/runtime-parser/src/test/java/org/enso/compiler/core/ParserDependenciesTest.java index 7f920a52980b..b6299f79e2fb 100644 --- a/engine/runtime-parser/src/test/java/org/enso/compiler/core/ParserDependenciesTest.java +++ b/engine/runtime-parser/src/test/java/org/enso/compiler/core/ParserDependenciesTest.java @@ -1,6 +1,10 @@ package org.enso.compiler.core; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; import org.junit.Test; @@ -37,4 +41,14 @@ public void avoidPolyglotDependency() { // correct } } + + @Test + public void parserProcessorIsAvailable() { + try { + var clazz = Class.forName("org.enso.runtime.parser.processor.IRProcessor"); + assertThat(clazz, is(notNullValue())); + } catch (ClassNotFoundException e) { + fail(e.getMessage()); + } + } } diff --git a/project/PackageListPlugin.scala b/project/PackageListPlugin.scala index 308e16d17547..298bc6644f36 100644 --- a/project/PackageListPlugin.scala +++ b/project/PackageListPlugin.scala @@ -61,9 +61,11 @@ object PackageListPlugin extends AutoPlugin { if (file.isFile) { val fPath = file.toPath val relativePath = rootDir.relativize(fPath) - val pkgPart = relativePath.subpath(0, relativePath.getNameCount - 1) - val pkg = pkgPart.toString.replace(File.separator, ".") - pkgs.add(pkg) + if (relativePath.getNameCount > 1) { + val pkgPart = relativePath.subpath(0, relativePath.getNameCount - 1) + val pkg = pkgPart.toString.replace(File.separator, ".") + pkgs.add(pkg) + } } else if (file.isDirectory) { listOfPackages(rootDir, file, pkgs) }