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:
+ *
+ *
+ * - {@code private DiagnosticStorage diagnostics}
+ *
- {@code private MetadataStorage passData}
+ *
- {@code private IdentifiedLocation location}
+ *
- {@code private UUID id}
+ *
+ *
+ * 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:
+ *
+ *
+ * - Any reference, or primitive type annotated with {@link IRField}
+ *
- A subtype of {@code org.enso.compiler.ir.IR} annotated with {@link IRChild}
+ *
- One of the meta types mentioned above
+ *
+ *
+ * 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 extends AnnotationValue> 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 extends TypeElement> 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 extends Annotation> 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)
}