diff --git a/build.sbt b/build.sbt
index e588843a702f..1fa62d92211d 100644
--- a/build.sbt
+++ b/build.sbt
@@ -353,6 +353,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`,
@@ -3213,14 +3216,72 @@ 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 processor, because
+ // one of the 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
+ )
+
+lazy val `runtime-parser-processor-tests` =
+ (project in file("engine/runtime-parser-processor-tests"))
+ .settings(
+ inConfig(Compile)(truffleRunOptionsSettings),
+ frgaalJavaCompilerSetting,
+ 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
+ )
+ )
+ .dependsOn(`runtime-parser-processor`)
+ .dependsOn(`runtime-parser`)
+
+lazy val `runtime-parser-processor` =
+ (project in file("engine/runtime-parser-processor"))
+ .enablePlugins(JPMSPlugin)
+ .settings(
+ frgaalJavaCompilerSetting,
+ 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..75953dff7807 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,9 @@ case object LambdaConsolidate extends IRPass {
}
val shadower: IR =
- mShadower.getOrElse(Empty(spec.identifiedLocation))
+ mShadower.getOrElse(
+ Empty.createFromLocation(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..0383e9c09bd6 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))
+ )(Empty.createEmpty())
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..5ae324828226 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),
+ Empty.createEmpty(),
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 e387e7908f44..45f5afa77dad 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 = CallArgument.Specified(None, Empty(null), false, null)
+ val left = Empty.createEmpty()
+ val right = Empty.createEmpty()
+ val rightArg = CallArgument.Specified(None, Empty.createEmpty(), 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 = Empty.createEmpty()
+ val right = Empty.createEmpty()
val rightArg = CallArgument.Specified(
None,
- Empty(identifiedLocation = null),
+ Empty.createEmpty(),
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/IRChild.java b/engine/runtime-parser-dsl/src/main/java/org/enso/runtime/parser/dsl/IRChild.java
new file mode 100644
index 000000000000..fe31bb942289
--- /dev/null
+++ b/engine/runtime-parser-dsl/src/main/java/org/enso/runtime/parser/dsl/IRChild.java
@@ -0,0 +1,20 @@
+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;
+
+/**
+ * Abstract methods annotated with this annotation will return the child of the current
+ * IR element (the current IR element is the interface annotated with {@link IRNode} that encloses
+ * this method). 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 method must have no parameters and
+ * return a subtype of {@code org.enso.compiler.ir.IR}.
+ */
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.METHOD)
+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/IRCopyMethod.java b/engine/runtime-parser-dsl/src/main/java/org/enso/runtime/parser/dsl/IRCopyMethod.java
new file mode 100644
index 000000000000..9895523cbf54
--- /dev/null
+++ b/engine/runtime-parser-dsl/src/main/java/org/enso/runtime/parser/dsl/IRCopyMethod.java
@@ -0,0 +1,30 @@
+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;
+
+/**
+ * An abstract method annotated with this annotation will have generated implementation in the
+ * generated class. The types and names of the parameters must correspond to any field (abstract
+ * parameterless methods in the interface hierarchy), or to any of the following:
+ *
+ *
+ * - {@code MetadataStorage passData}
+ *
- {@code IdentifiedLocation location}
+ *
- {@code UUID id}
+ *
+ *
+ * The order of the parameters is not important. Number of parameters must not exceed total number
+ * of fields and meta fields.
+ *
+ * There can be more methods annotated with this annotation. All of them must follow the contract
+ * describe in this docs. For each of those methods, an implementation will be generated.
+ *
+ *
The name of the annotated method can be arbitrary, but the convention is to use the {@code
+ * copy} name.
+ */
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.METHOD)
+public @interface IRCopyMethod {}
diff --git a/engine/runtime-parser-dsl/src/main/java/org/enso/runtime/parser/dsl/IRNode.java b/engine/runtime-parser-dsl/src/main/java/org/enso/runtime/parser/dsl/IRNode.java
new file mode 100644
index 000000000000..a8aaa434331b
--- /dev/null
+++ b/engine/runtime-parser-dsl/src/main/java/org/enso/runtime/parser/dsl/IRNode.java
@@ -0,0 +1,23 @@
+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;
+
+/**
+ * An interface annotated with this annotation will be processed by the IR processor. The processor
+ * will generate a class that extends this interface. The generated class will have the same package
+ * as this interface, and its name will have the "Gen" suffix. The interface must be a subtype of
+ * {@code org.enso.compiler.ir.IR}. The interface can contain {@link IRChild} and {@link
+ * IRCopyMethod} annotated methods.
+ *
+ *
For every abstract parameterless method of the interface, there will be a field in the
+ * generated class.
+ *
+ *
The interface can contain arbitrary number of nested interfaces. In such case, the processor
+ * will generate nested static classes for all these nested interfaces.
+ */
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.TYPE)
+public @interface IRNode {}
diff --git a/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/CopyNameTestIR.java b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/CopyNameTestIR.java
new file mode 100644
index 000000000000..14e1c4fb6d19
--- /dev/null
+++ b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/CopyNameTestIR.java
@@ -0,0 +1,19 @@
+package org.enso.runtime.parser.processor.test.gen.ir;
+
+import org.enso.compiler.core.IR;
+import org.enso.runtime.parser.dsl.IRChild;
+import org.enso.runtime.parser.dsl.IRCopyMethod;
+import org.enso.runtime.parser.dsl.IRNode;
+
+@IRNode
+public interface CopyNameTestIR extends IR {
+ @IRChild
+ NameTestIR name();
+
+ /**
+ * Should generate implementation that will produce the exact same copy with a different name
+ * field.
+ */
+ @IRCopyMethod
+ CopyNameTestIR copy(NameTestIR name);
+}
diff --git a/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/ListTestIR.java b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/ListTestIR.java
new file mode 100644
index 000000000000..1fd580d00d33
--- /dev/null
+++ b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/ListTestIR.java
@@ -0,0 +1,15 @@
+package org.enso.runtime.parser.processor.test.gen.ir;
+
+import org.enso.compiler.core.IR;
+import org.enso.runtime.parser.dsl.IRChild;
+import org.enso.runtime.parser.dsl.IRNode;
+import scala.collection.immutable.List;
+
+@IRNode
+public interface ListTestIR extends IR {
+ @IRChild
+ List names();
+
+ @IRChild(required = false)
+ NameTestIR originalName();
+}
diff --git a/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/NameTestIR.java b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/NameTestIR.java
new file mode 100644
index 000000000000..c03a5b069323
--- /dev/null
+++ b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/NameTestIR.java
@@ -0,0 +1,10 @@
+package org.enso.runtime.parser.processor.test.gen.ir;
+
+import org.enso.compiler.core.IR;
+import org.enso.runtime.parser.dsl.IRNode;
+
+@IRNode
+public interface NameTestIR extends IR {
+
+ String name();
+}
diff --git a/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/OptNameTestIR.java b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/OptNameTestIR.java
new file mode 100644
index 000000000000..b6443ec5031e
--- /dev/null
+++ b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/OptNameTestIR.java
@@ -0,0 +1,11 @@
+package org.enso.runtime.parser.processor.test.gen.ir;
+
+import org.enso.compiler.core.IR;
+import org.enso.runtime.parser.dsl.IRChild;
+import org.enso.runtime.parser.dsl.IRNode;
+
+@IRNode
+public interface OptNameTestIR extends IR {
+ @IRChild(required = false)
+ OptNameTestIR originalName();
+}
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..150949228daf
--- /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,18 @@
+package org.enso.runtime.parser.processor.test.gen.ir.core;
+
+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.IRChild;
+import org.enso.runtime.parser.dsl.IRNode;
+
+@IRNode
+public interface JCallArgument extends IR {
+ @IRChild(required = false)
+ Name name();
+
+ @IRChild
+ Expression value();
+
+ interface JSpecified extends JCallArgument {}
+}
diff --git a/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/core/JDefinitionArgument.java b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/core/JDefinitionArgument.java
new file mode 100644
index 000000000000..37cae56ec8d5
--- /dev/null
+++ b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/core/JDefinitionArgument.java
@@ -0,0 +1,21 @@
+package org.enso.runtime.parser.processor.test.gen.ir.core;
+
+import org.enso.compiler.core.IR;
+import org.enso.runtime.parser.dsl.IRChild;
+import org.enso.runtime.parser.dsl.IRNode;
+
+@IRNode
+public interface JDefinitionArgument extends IR {
+ @IRChild
+ JName name();
+
+ @IRChild(required = false)
+ JExpression ascribedType();
+
+ @IRChild(required = false)
+ JExpression defaultValue();
+
+ boolean suspended();
+
+ interface JSpecified extends JDefinitionArgument {}
+}
diff --git a/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/core/JEmpty.java b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/core/JEmpty.java
new file mode 100644
index 000000000000..59d51aa08614
--- /dev/null
+++ b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/core/JEmpty.java
@@ -0,0 +1,36 @@
+package org.enso.runtime.parser.processor.test.gen.ir.core;
+
+import java.util.UUID;
+import org.enso.compiler.core.IR;
+import org.enso.compiler.core.ir.DiagnosticStorage;
+import org.enso.compiler.core.ir.IdentifiedLocation;
+import org.enso.compiler.core.ir.MetadataStorage;
+import org.enso.runtime.parser.dsl.IRCopyMethod;
+import org.enso.runtime.parser.dsl.IRNode;
+
+@IRNode
+public interface JEmpty extends IR {
+ static JEmptyGen.Builder builder() {
+ return JEmptyGen.builder();
+ }
+
+ static JEmpty createEmpty() {
+ return JEmptyGen.builder().build();
+ }
+
+ static JEmpty createFromLocation(IdentifiedLocation location) {
+ return JEmptyGen.builder().location(location).build();
+ }
+
+ static JEmpty createFromLocationAndPassData(
+ IdentifiedLocation location, MetadataStorage passData) {
+ return JEmptyGen.builder().location(location).passData(passData).build();
+ }
+
+ @IRCopyMethod
+ JEmpty copy(
+ IdentifiedLocation location,
+ MetadataStorage passData,
+ DiagnosticStorage diagnostics,
+ UUID id);
+}
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..bf6fe1f79fb1
--- /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,46 @@
+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.runtime.parser.dsl.IRChild;
+import org.enso.runtime.parser.dsl.IRNode;
+import scala.collection.immutable.List;
+
+@IRNode
+public interface JExpression extends IR {
+
+ @Override
+ JExpression mapExpressions(Function fn);
+
+ @Override
+ JExpression duplicate(
+ boolean keepLocations,
+ boolean keepMetadata,
+ boolean keepDiagnostics,
+ boolean keepIdentifiers);
+
+ interface JBlock extends JExpression {
+ @IRChild
+ List expressions();
+
+ @IRChild
+ JExpression returnValue();
+
+ boolean suspended();
+ }
+
+ /**
+ * A binding expression of the form `name = expr`
+ *
+ * To create a binding that binds no available name, set the name of the binding to an
+ * [[Name.Blank]] (e.g. _ = foo a b).
+ */
+ interface JBinding extends JExpression {
+ @IRChild
+ JName name();
+
+ @IRChild
+ JExpression expression();
+ }
+}
diff --git a/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/core/JModule.java b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/core/JModule.java
new file mode 100644
index 000000000000..4162b197882b
--- /dev/null
+++ b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/core/JModule.java
@@ -0,0 +1,17 @@
+package org.enso.runtime.parser.processor.test.gen.ir.core;
+
+import org.enso.compiler.core.IR;
+import org.enso.runtime.parser.dsl.IRChild;
+import org.enso.runtime.parser.dsl.IRNode;
+import org.enso.runtime.parser.processor.test.gen.ir.module.scope.JExport;
+import org.enso.runtime.parser.processor.test.gen.ir.module.scope.JImport;
+import scala.collection.immutable.List;
+
+@IRNode
+public interface JModule extends IR {
+ @IRChild
+ List imports();
+
+ @IRChild
+ List exports();
+}
diff --git a/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/core/JName.java b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/core/JName.java
new file mode 100644
index 000000000000..58c07d32e934
--- /dev/null
+++ b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/core/JName.java
@@ -0,0 +1,52 @@
+package org.enso.runtime.parser.processor.test.gen.ir.core;
+
+import org.enso.runtime.parser.dsl.IRChild;
+import org.enso.runtime.parser.dsl.IRNode;
+import org.enso.runtime.parser.processor.test.gen.ir.module.scope.JDefinition;
+import scala.collection.immutable.List;
+
+@IRNode
+public interface JName extends JExpression {
+ String name();
+
+ boolean isMethod();
+
+ @Override
+ JName duplicate(
+ boolean keepLocations,
+ boolean keepMetadata,
+ boolean keepDiagnostics,
+ boolean keepIdentifiers);
+
+ interface JBlank extends JName {
+ static JBlank create() {
+ return JNameGen.JBlankGen.builder().build();
+ }
+ }
+
+ interface JLiteral extends JName {
+ @IRChild(required = false)
+ JName originalName();
+ }
+
+ interface JQualified extends JName {
+ @IRChild
+ List parts();
+
+ @Override
+ default String name() {
+ return parts().map(JName::name).mkString(".");
+ }
+ }
+
+ interface JSelf extends JName {
+ boolean synthetic();
+ }
+
+ interface JAnnotation extends JName, JDefinition {}
+
+ interface JGenericAnnotation extends JAnnotation {
+ @IRChild
+ JExpression 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/module/JScope.java b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/module/JScope.java
new file mode 100644
index 000000000000..6742b1839e99
--- /dev/null
+++ b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/module/JScope.java
@@ -0,0 +1,8 @@
+package org.enso.runtime.parser.processor.test.gen.ir.module;
+
+import org.enso.compiler.core.IR;
+import org.enso.runtime.parser.dsl.IRNode;
+
+/** A representation of constructs that can only occur in the top-level module scope */
+@IRNode
+public interface JScope extends IR {}
diff --git a/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/module/scope/JDefinition.java b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/module/scope/JDefinition.java
new file mode 100644
index 000000000000..42c0be0292b7
--- /dev/null
+++ b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/module/scope/JDefinition.java
@@ -0,0 +1,57 @@
+package org.enso.runtime.parser.processor.test.gen.ir.module.scope;
+
+import org.enso.compiler.core.IR;
+import org.enso.runtime.parser.dsl.IRChild;
+import org.enso.runtime.parser.dsl.IRNode;
+import org.enso.runtime.parser.processor.test.gen.ir.core.JDefinitionArgument;
+import org.enso.runtime.parser.processor.test.gen.ir.core.JName;
+import org.enso.runtime.parser.processor.test.gen.ir.module.JScope;
+import scala.collection.immutable.List;
+
+@IRNode
+public interface JDefinition extends JScope {
+ interface JType extends JDefinition {
+ @IRChild
+ JName name();
+
+ @IRChild
+ List params();
+
+ @IRChild
+ List members();
+ }
+
+ /** The definition of an atom constructor and its associated arguments. */
+ interface JData extends JDefinition {
+ /** The name of the atom */
+ @IRChild
+ JName name();
+
+ /** The arguments of the atom constructor. */
+ @IRChild
+ List arguments();
+
+ @IRChild
+ List annotations();
+
+ /** If the constructor is project-private. */
+ boolean isPrivate();
+ }
+
+ /**
+ * The definition of a complex type definition that may contain multiple atom and method
+ * definitions.
+ */
+ interface JSugaredType extends JDefinition {
+
+ /** The name of the complex type. */
+ @IRChild
+ JName name();
+
+ @IRChild
+ List arguments();
+
+ @IRChild
+ List body();
+ }
+}
diff --git a/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/module/scope/JExport.java b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/module/scope/JExport.java
new file mode 100644
index 000000000000..53b26778d147
--- /dev/null
+++ b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/module/scope/JExport.java
@@ -0,0 +1,53 @@
+package org.enso.runtime.parser.processor.test.gen.ir.module.scope;
+
+import org.enso.runtime.parser.dsl.IRChild;
+import org.enso.runtime.parser.dsl.IRNode;
+import org.enso.runtime.parser.processor.test.gen.ir.core.JName;
+import org.enso.runtime.parser.processor.test.gen.ir.module.JScope;
+import scala.collection.immutable.List;
+
+@IRNode
+public interface JExport extends JScope {
+ interface JModule extends JExport {
+ @IRChild
+ JName.JQualified name();
+
+ @IRChild(required = false)
+ JName.JLiteral rename();
+
+ @IRChild(required = false)
+ List onlyNames();
+
+ boolean isSynthetic();
+
+ /**
+ * Gets the name of the module visible in the importing scope, either the original name or the
+ * rename.
+ *
+ * @return the name of this export visible in code
+ */
+ default JName getSimpleName() {
+ if (rename() != null) {
+ return rename();
+ } else {
+ return name().parts().apply(name().parts().size() - 1);
+ }
+ }
+
+ /**
+ * Checks whether the export statement allows use of the given exported name.
+ *
+ * Note that it does not verify if the name is actually exported by the module, only checks
+ * if it is syntactically allowed.
+ *
+ * @param name the name to check
+ * @return whether the name could be accessed or not
+ */
+ default boolean allowsAccess(String name) {
+ if (onlyNames() != null) {
+ return onlyNames().exists(n -> n.name().equalsIgnoreCase(name));
+ }
+ return true;
+ }
+ }
+}
diff --git a/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/module/scope/JImport.java b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/module/scope/JImport.java
new file mode 100644
index 000000000000..ff74f509324c
--- /dev/null
+++ b/engine/runtime-parser-processor-tests/src/main/java/org/enso/runtime/parser/processor/test/gen/ir/module/scope/JImport.java
@@ -0,0 +1,51 @@
+package org.enso.runtime.parser.processor.test.gen.ir.module.scope;
+
+import org.enso.runtime.parser.dsl.IRChild;
+import org.enso.runtime.parser.dsl.IRNode;
+import org.enso.runtime.parser.processor.test.gen.ir.core.JName;
+import org.enso.runtime.parser.processor.test.gen.ir.module.JScope;
+import scala.collection.immutable.List;
+
+/** Module-level import statements. */
+@IRNode
+public interface JImport extends JScope {
+ interface JModule extends JImport {
+ @IRChild
+ JName.JQualified name();
+
+ @IRChild(required = false)
+ JName.JLiteral rename();
+
+ boolean isAll();
+
+ @IRChild(required = false)
+ List onlyNames();
+
+ @IRChild(required = false)
+ List hiddenNames();
+
+ boolean isSynthetic();
+
+ /**
+ * Checks whether the import statement allows use of the given exported name.
+ *
+ * Note that it does not verify if the name is actually exported by the module, only checks
+ * if it is syntactically allowed.
+ *
+ * @param name the name to check
+ * @return whether the name could be accessed or not
+ */
+ default boolean allowsAccess(String name) {
+ if (!isAll()) {
+ return false;
+ }
+ if (onlyNames() != null) {
+ return onlyNames().exists(n -> n.name().equals(name));
+ }
+ if (hiddenNames() != null) {
+ return hiddenNames().forall(n -> !n.name().equals(name));
+ }
+ return true;
+ }
+ }
+}
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/TestGeneratedIR.java b/engine/runtime-parser-processor-tests/src/test/java/org/enso/runtime/parser/processor/test/TestGeneratedIR.java
new file mode 100644
index 000000000000..728caa56bae8
--- /dev/null
+++ b/engine/runtime-parser-processor-tests/src/test/java/org/enso/runtime/parser/processor/test/TestGeneratedIR.java
@@ -0,0 +1,128 @@
+package org.enso.runtime.parser.processor.test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+import org.enso.compiler.core.ir.DiagnosticStorage;
+import org.enso.compiler.core.ir.IdentifiedLocation;
+import org.enso.compiler.core.ir.Location;
+import org.enso.runtime.parser.processor.test.gen.ir.CopyNameTestIRGen;
+import org.enso.runtime.parser.processor.test.gen.ir.ListTestIR;
+import org.enso.runtime.parser.processor.test.gen.ir.ListTestIRGen;
+import org.enso.runtime.parser.processor.test.gen.ir.NameTestIR;
+import org.enso.runtime.parser.processor.test.gen.ir.NameTestIRGen;
+import org.enso.runtime.parser.processor.test.gen.ir.OptNameTestIRGen;
+import org.junit.Test;
+
+public class TestGeneratedIR {
+ @Test
+ public void generatedCodeHasBuilder() {
+ NameTestIR myIr = NameTestIRGen.builder().name("name").build();
+ assertThat(myIr.name(), is("name"));
+ }
+
+ @Test
+ public void myIRHasNoChildren() {
+ NameTestIR myIr = NameTestIRGen.builder().name("name").build();
+ assertThat(myIr.children().isEmpty(), is(true));
+ }
+
+ @Test
+ public void nonChildFieldIsNotNullable() {
+ var bldr = NameTestIRGen.builder();
+ try {
+ bldr.build();
+ fail("Expected exception - name field must be specified in the builder");
+ } catch (Exception e) {
+ assertThat(e, is(notNullValue()));
+ }
+ }
+
+ @Test
+ public void canDuplicate() {
+ NameTestIR myIr = NameTestIRGen.builder().name("name").build();
+ var duplicated = myIr.duplicate(true, true, true, true);
+ assertThat("duplicate returns same type", duplicated, instanceOf(NameTestIR.class));
+ assertThat("name was correctly duplicated", ((NameTestIR) duplicated).name(), is("name"));
+ }
+
+ @Test
+ public void generatedBuilderCanSetMetadata() {
+ var diagnostics = DiagnosticStorage.empty();
+ var nameIR = NameTestIRGen.builder().name("name").diagnostics(diagnostics).build();
+ assertThat(nameIR.diagnostics(), is(diagnostics));
+ }
+
+ @Test
+ public void canCreateList() {
+ var firstName = NameTestIRGen.builder().name("first_name").build();
+ var secondName = NameTestIRGen.builder().name("second_name").build();
+ scala.collection.immutable.List names = asScala(List.of(firstName, secondName));
+ var listIr = ListTestIRGen.builder().names(names).build();
+ assertThat(listIr.names().size(), is(2));
+ }
+
+ @Test
+ public void canGetListAsChildren() {
+ var firstName = NameTestIRGen.builder().name("first_name").build();
+ var secondName = NameTestIRGen.builder().name("second_name").build();
+ scala.collection.immutable.List names = asScala(List.of(firstName, secondName));
+ var listIr = ListTestIRGen.builder().names(names).build();
+ assertThat(listIr.children().size(), is(2));
+ assertThat(listIr.children().head(), instanceOf(NameTestIR.class));
+ }
+
+ @Test
+ public void canDuplicateListTestIR() {
+ var firstName = NameTestIRGen.builder().name("first_name").build();
+ var secondName = NameTestIRGen.builder().name("second_name").build();
+ scala.collection.immutable.List names = asScala(List.of(firstName, secondName));
+ var listIr = ListTestIRGen.builder().names(names).build();
+ var duplicated = listIr.duplicate(true, true, true, true);
+ assertThat(duplicated, instanceOf(ListTestIR.class));
+ assertThat(duplicated.children().size(), is(2));
+ }
+
+ @Test
+ public void optChildIsNotRequired() {
+ var optNameTestIR = OptNameTestIRGen.builder().build();
+ assertThat(optNameTestIR, is(notNullValue()));
+ assertThat(optNameTestIR.originalName(), is(nullValue()));
+ }
+
+ @Test
+ public void duplicateRespectsParameters() {
+ var location = new IdentifiedLocation(new Location(1, 2));
+ var diagnostics = DiagnosticStorage.empty();
+ var nameIR =
+ NameTestIRGen.builder().name("name").location(location).diagnostics(diagnostics).build();
+ var duplicated = nameIR.duplicate(true, false, false, false);
+ assertThat("Should have copied location meta", duplicated.location().isDefined(), is(true));
+ assertThat("Should have not copied diagnostics", duplicated.diagnostics(), is(nullValue()));
+
+ var duplicated_2 = nameIR.duplicate(false, false, true, false);
+ assertThat(
+ "Should have not copied location meta", duplicated_2.location().isDefined(), is(false));
+ assertThat("Should have copied diagnostics", duplicated_2.diagnostics(), is(notNullValue()));
+ }
+
+ @Test
+ public void copyMethod() {
+ var diagnostics = DiagnosticStorage.empty();
+ var nameIR = NameTestIRGen.builder().name("name").build();
+ var copyNameIR = CopyNameTestIRGen.builder().diagnostics(diagnostics).name(nameIR).build();
+ var otherNameIR = NameTestIRGen.builder().name("other_name").build();
+ var copied = copyNameIR.copy(otherNameIR);
+ assertThat(copied.name().name(), is("other_name"));
+ assertThat("Diagnostics should have been copied", copied.diagnostics(), is(diagnostics));
+ }
+
+ private static scala.collection.immutable.List asScala(List list) {
+ return scala.jdk.javaapi.CollectionConverters.asScala(list).toList();
+ }
+}
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..28e464dcf17e
--- /dev/null
+++ b/engine/runtime-parser-processor-tests/src/test/java/org/enso/runtime/parser/processor/test/TestIRProcessorInline.java
@@ -0,0 +1,569 @@
+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 static org.hamcrest.Matchers.not;
+
+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();
+ }
+
+ @Test
+ public void simpleIRNodeWithoutChildren_CompilationSucceeds() {
+ var src =
+ JavaFileObjects.forSourceString(
+ "JName",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.compiler.core.IR;
+ @IRNode
+ public interface JName extends IR {}
+ """);
+ var compiler = Compiler.javac().withProcessors(new IRProcessor());
+ var compilation = compiler.compile(src);
+ CompilationSubject.assertThat(compilation).succeeded();
+ }
+
+ @Test
+ public void annotatedInterfaceMustExtendIR() {
+ var src =
+ JavaFileObjects.forSourceString(
+ "Hello",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ @IRNode
+ public interface Hello {}
+ """);
+ var compiler = Compiler.javac().withProcessors(new IRProcessor());
+ var compilation = compiler.compile(src);
+ CompilationSubject.assertThat(compilation).failed();
+ }
+
+ @Test
+ public void annotatedInterfaceMustNotExtendTwoIRInterfaces() {
+ var src =
+ JavaFileObjects.forSourceString(
+ "Hello",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.compiler.core.IR;
+ import org.enso.compiler.core.ir.Expression;
+
+ @IRNode
+ public interface Hello extends IR, Expression {}
+ """);
+ 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("must extend only a single IR interface");
+ }
+
+ @Test
+ public void annotationCanOnlyBeAppliedToInterface() {
+ var src =
+ JavaFileObjects.forSourceString(
+ "Hello",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ @IRNode
+ public class Hello {}
+ """);
+ var compiler = Compiler.javac().withProcessors(new IRProcessor());
+ var compilation = compiler.compile(src);
+ CompilationSubject.assertThat(compilation).failed();
+ }
+
+ @Test
+ public void childAnnotation_MustBeAppliedToIRField() {
+ expectCompilationFailure(
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.runtime.parser.dsl.IRChild;
+ import org.enso.compiler.core.IR;
+
+ @IRNode
+ public interface MyIR extends IR {
+ @IRChild String expression();
+ }
+ """);
+ }
+
+ @Test
+ public void simpleIRNodeWithoutChildren_GeneratesSource() {
+ var src =
+ JavaFileObjects.forSourceString(
+ "JName",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.compiler.core.IR;
+ @IRNode
+ public interface JName extends IR {}
+ """);
+ var compiler = Compiler.javac().withProcessors(new IRProcessor());
+ var compilation = compiler.compile(src);
+ CompilationSubject.assertThat(compilation).succeeded();
+ CompilationSubject.assertThat(compilation).generatedSourceFile("JNameGen").isNotNull();
+ var srcSubject =
+ CompilationSubject.assertThat(compilation)
+ .generatedSourceFile("JNameGen")
+ .contentsAsUtf8String();
+ srcSubject.containsMatch("");
+ var genSrc = compilation.generatedSourceFile("JNameGen");
+ assertThat(genSrc.isPresent(), is(true));
+ assertThat("Generated just one source", compilation.generatedSourceFiles().size(), is(1));
+ }
+
+ @Test
+ public void doesNotOverrideStaticParameterlessMethod() {
+ var src =
+ generatedClass(
+ "Hello",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.compiler.core.IR;
+
+ @IRNode
+ public interface Hello extends IR {
+ static String name() {
+ return "Hello";
+ }
+ }
+ """);
+ assertThat(src, not(containsString("\"Hello\"")));
+ }
+
+ @Test
+ public void simpleIRNodeWithChild() {
+ var genSrc =
+ generatedClass(
+ "MyIR",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.runtime.parser.dsl.IRChild;
+ import org.enso.compiler.core.IR;
+ import org.enso.compiler.core.ir.Expression;
+
+ @IRNode
+ public interface MyIR extends IR {
+ @IRChild Expression expression();
+ }
+ """);
+ assertThat(genSrc, containsString("Expression expression()"));
+ }
+
+ @Test
+ public void irNodeWithMultipleFields_PrimitiveField() {
+ var genSrc =
+ generatedClass(
+ "MyIR",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.runtime.parser.dsl.IRChild;
+ import org.enso.compiler.core.IR;
+
+ @IRNode
+ public interface MyIR extends IR {
+ boolean suspended();
+ }
+ """);
+ assertThat(genSrc, containsString("boolean suspended()"));
+ }
+
+ @Test
+ public void irNodeWithInheritedField() {
+ var src =
+ generatedClass(
+ "MyIR",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.runtime.parser.dsl.IRChild;
+ import org.enso.compiler.core.IR;
+
+ interface MySuperIR extends IR {
+ boolean suspended();
+ }
+
+ @IRNode
+ public interface MyIR extends MySuperIR {
+ }
+
+ """);
+ assertThat(src, containsString("boolean suspended()"));
+ }
+
+ @Test
+ public void irNodeWithInheritedField_Override() {
+ var src =
+ generatedClass(
+ "MyIR",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.runtime.parser.dsl.IRChild;
+ import org.enso.compiler.core.IR;
+
+ interface MySuperIR extends IR {
+ boolean suspended();
+ }
+
+ @IRNode
+ public interface MyIR extends MySuperIR {
+ boolean suspended();
+ }
+
+ """);
+ assertThat(src, containsString("boolean suspended()"));
+ }
+
+ @Test
+ public void irNodeWithInheritedField_Transitive() {
+ var src =
+ generatedClass(
+ "MyIR",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.runtime.parser.dsl.IRChild;
+ import org.enso.compiler.core.IR;
+
+ interface MySuperSuperIR extends IR {
+ boolean suspended();
+ }
+
+ interface MySuperIR extends MySuperSuperIR {
+ }
+
+ @IRNode
+ public interface MyIR extends MySuperIR {
+ }
+ """);
+ assertThat(src, containsString("boolean suspended()"));
+ }
+
+ @Test
+ public void irNodeAsNestedInterface() {
+ var src =
+ generatedClass(
+ "JName",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.compiler.core.IR;
+
+ @IRNode
+ public interface JName extends IR {
+ String name();
+
+ interface JBlank extends JName {}
+ }
+ """);
+ assertThat(src, containsString("public final class JNameGen"));
+ assertThat(src, containsString("public static final class JBlankGen implements JName.JBlank"));
+ }
+
+ @Test
+ public void returnValueCanBeScalaList() {
+ var src =
+ generatedClass(
+ "JName",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.runtime.parser.dsl.IRChild;
+ import org.enso.compiler.core.IR;
+ import scala.collection.immutable.List;
+
+ @IRNode
+ public interface JName extends IR {
+ @IRChild
+ List expressions();
+ }
+ """);
+ assertThat(src, containsString("public final class JNameGen"));
+ assertThat(src, containsString("List expressions"));
+ }
+
+ @Test
+ public void processorDoesNotGenerateOverridenMethods() {
+ var src =
+ generatedClass(
+ "JName",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.compiler.core.IR;
+
+ @IRNode
+ public interface JName extends IR {
+ String name();
+
+ interface JQualified extends JName {
+ @Override
+ default String name() {
+ return null;
+ }
+ }
+ }
+ """);
+ assertThat(src, containsString("public final class JNameGen"));
+ assertThat(src, not(containsString("String name()")));
+ }
+
+ @Test
+ public void overrideCorrectMethods() {
+ var src =
+ generatedClass(
+ "JExpression",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.compiler.core.IR;
+
+ @IRNode
+ public interface JExpression extends IR {
+
+ interface JBlock extends JExpression {
+ boolean suspended();
+ }
+
+ interface JBinding extends JExpression {
+ String name();
+ }
+ }
+ """);
+ assertThat(src, containsString("class JBlockGen"));
+ assertThat(src, containsString("class JBindingGen"));
+ }
+
+ @Test
+ public void canOverrideMethodsFromIR() {
+ var src =
+ generatedClass(
+ "JName",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.compiler.core.IR;
+
+ @IRNode
+ public interface JName extends IR {
+ @Override
+ JName duplicate(boolean keepLocations, boolean keepMetadata, boolean keepDiagnostics, boolean keepIdentifiers);
+
+ interface JSelf extends JName {}
+ }
+ """);
+ assertThat(src, containsString("JName duplicate"));
+ assertThat(src, containsString("JSelfGen"));
+ }
+
+ @Test
+ public void canDefineCopyMethod_WithUserDefinedField() {
+ var genSrc =
+ generatedClass(
+ "JName",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.runtime.parser.dsl.IRCopyMethod;
+ import org.enso.compiler.core.IR;
+
+ @IRNode
+ public interface JName extends IR {
+ String nameField();
+
+ @IRCopyMethod
+ JName copy(String nameField);
+ }
+ """);
+ assertThat(genSrc, containsString("JName copy(String nameField"));
+ }
+
+ @Test
+ public void canDefineCopyMethod_WithMetaField() {
+ var genSrc =
+ generatedClass(
+ "JName",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.runtime.parser.dsl.IRCopyMethod;
+ import org.enso.compiler.core.IR;
+ import org.enso.compiler.core.ir.MetadataStorage;
+
+ @IRNode
+ public interface JName extends IR {
+ String nameField();
+
+ @IRCopyMethod
+ JName copy(MetadataStorage passData);
+ }
+ """);
+ assertThat(genSrc, containsString("JName copy(MetadataStorage"));
+ }
+
+ @Test
+ public void canDefineMultipleCopyMethods() {
+ var genSrc =
+ generatedClass(
+ "JName",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.runtime.parser.dsl.IRCopyMethod;
+ import org.enso.compiler.core.IR;
+ import org.enso.compiler.core.ir.MetadataStorage;
+
+ @IRNode
+ public interface JName extends IR {
+ String nameField();
+
+ @IRCopyMethod
+ JName copy(MetadataStorage passData);
+
+ @IRCopyMethod
+ JName copy(String nameField);
+ }
+ """);
+ assertThat(genSrc, containsString("JName copy(MetadataStorage"));
+ assertThat(genSrc, containsString("JName copy(String"));
+ }
+
+ @Test
+ public void copyMethod_WithArbitraryArgumentOrder() {
+ var genSrc =
+ generatedClass(
+ "JName",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.runtime.parser.dsl.IRCopyMethod;
+ import org.enso.compiler.core.IR;
+ import org.enso.compiler.core.ir.MetadataStorage;
+ import org.enso.compiler.core.ir.DiagnosticStorage;
+
+ @IRNode
+ public interface JName extends IR {
+ String nameField();
+
+ @IRCopyMethod
+ JName copy(String nameField, MetadataStorage passData, DiagnosticStorage diagnostics);
+ }
+ """);
+ assertThat(genSrc, containsString("JName copy("));
+ }
+
+ @Test
+ public void copyMethod_MustContainValidFieldsAsParameters_1() {
+ expectCompilationFailure(
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.runtime.parser.dsl.IRCopyMethod;
+ import org.enso.compiler.core.IR;
+ import org.enso.compiler.core.ir.MetadataStorage;
+ import org.enso.compiler.core.ir.DiagnosticStorage;
+
+ @IRNode
+ public interface JName extends IR {
+ String nameField();
+
+ @IRCopyMethod
+ JName copy(String NON_EXISTING_FIELD_NAME);
+ }
+ """);
+ }
+
+ @Test
+ public void copyMethod_MustContainValidFieldsAsParameters_2() {
+ expectCompilationFailure(
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.runtime.parser.dsl.IRCopyMethod;
+ import org.enso.compiler.core.IR;
+ import org.enso.compiler.core.ir.MetadataStorage;
+ import org.enso.compiler.core.ir.DiagnosticStorage;
+
+ @IRNode
+ public interface JName extends IR {
+ String nameField();
+
+ @IRCopyMethod
+ JName copy(String nameField, String ANOTHER_NON_EXISTING);
+ }
+ """);
+ }
+
+ @Test
+ public void copyMethod_WithMoreFieldsOfSameType() {
+ var genSrc =
+ generatedClass(
+ "JName",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.runtime.parser.dsl.IRCopyMethod;
+ import org.enso.compiler.core.IR;
+ import org.enso.compiler.core.ir.MetadataStorage;
+ import org.enso.compiler.core.ir.DiagnosticStorage;
+
+ @IRNode
+ public interface JName extends IR {
+ String nameField_1();
+ String nameField_2();
+
+ @IRCopyMethod
+ JName copy(String nameField_1, String nameField_2);
+ }
+ """);
+ assertThat(genSrc, containsString("JName copy("));
+ }
+
+ @Test
+ public void mapExpressions_CanOverride() {
+ var genSrc =
+ generatedClass(
+ "JExpression",
+ """
+ import org.enso.runtime.parser.dsl.IRNode;
+ import org.enso.compiler.core.IR;
+ import org.enso.compiler.core.ir.Expression;
+ import java.util.function.Function;
+
+ @IRNode
+ public interface JExpression extends IR {
+
+ @Override
+ JExpression mapExpressions(Function fn);
+ }
+ """);
+ assertThat(genSrc, containsString("JExpression mapExpressions("));
+ }
+}
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/GeneratedClassContext.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/GeneratedClassContext.java
new file mode 100644
index 000000000000..bf10c52b72f8
--- /dev/null
+++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/GeneratedClassContext.java
@@ -0,0 +1,136 @@
+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.TypeElement;
+import org.enso.runtime.parser.processor.field.Field;
+
+/**
+ * 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 constructorParameters;
+ private final ProcessingEnvironment processingEnvironment;
+ private final TypeElement irNodeInterface;
+
+ private static final ClassField diagnosticsMetaField =
+ new ClassField("private", "DiagnosticStorage", "diagnostics");
+ private static final ClassField passDataMetaField =
+ new ClassField("private", "MetadataStorage", "passData");
+ private static final ClassField locationMetaField =
+ new ClassField("private", "IdentifiedLocation", "location");
+ private static final ClassField idMetaField = new ClassField("private", "UUID", "id");
+
+ /** Meta fields are always present in the generated class. */
+ private static final List metaFields =
+ List.of(diagnosticsMetaField, passDataMetaField, locationMetaField, idMetaField);
+
+ /**
+ * @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.
+ * @param irNodeInterface Type element of the interface annotated with {@link
+ * org.enso.runtime.parser.dsl.IRNode} - for this interface the class is generated.
+ */
+ GeneratedClassContext(
+ String className,
+ List userFields,
+ ProcessingEnvironment processingEnvironment,
+ TypeElement irNodeInterface) {
+ this.className = Objects.requireNonNull(className);
+ this.userFields = Objects.requireNonNull(userFields);
+ this.processingEnvironment = Objects.requireNonNull(processingEnvironment);
+ this.irNodeInterface = irNodeInterface;
+ ensureSimpleName(className);
+ this.constructorParameters =
+ getAllFields().stream()
+ .map(classField -> new Parameter(classField.type(), classField.name()))
+ .toList();
+ }
+
+ private static void ensureSimpleName(String name) {
+ if (name.contains(".")) {
+ throw new IllegalArgumentException("Class name must be simple, not qualified");
+ }
+ }
+
+ public ClassField getLocationMetaField() {
+ return locationMetaField;
+ }
+
+ public ClassField getPassDataMetaField() {
+ return passDataMetaField;
+ }
+
+ public ClassField getDiagnosticsMetaField() {
+ return diagnosticsMetaField;
+ }
+
+ public ClassField getIdMetaField() {
+ return idMetaField;
+ }
+
+ public List getConstructorParameters() {
+ return constructorParameters;
+ }
+
+ public List getUserFields() {
+ return userFields;
+ }
+
+ /** Returns simple name of the class that is being generated. */
+ public String getClassName() {
+ return className;
+ }
+
+ List getMetaFields() {
+ return metaFields;
+ }
+
+ public List getAllFields() {
+ var allFields = new ArrayList(metaFields);
+ for (var userField : userFields) {
+ allFields.add(
+ new ClassField("private final", userField.getSimpleTypeName(), userField.getName()));
+ }
+ return allFields;
+ }
+
+ public ProcessingEnvironment getProcessingEnvironment() {
+ return processingEnvironment;
+ }
+
+ public TypeElement getIrNodeInterface() {
+ return irNodeInterface;
+ }
+
+ /**
+ * Method parameter
+ *
+ * @param type
+ * @param name
+ */
+ record Parameter(String type, String name) {
+ @Override
+ public String toString() {
+ return type + " " + name;
+ }
+ }
+
+ /**
+ * Declared field in the class
+ *
+ * @param modifiers
+ */
+ public record ClassField(String modifiers, String type, String name) {
+ @Override
+ public String toString() {
+ return modifiers + " " + type + " " + name;
+ }
+ }
+}
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..89101485442e
--- /dev/null
+++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/IRNodeClassGenerator.java
@@ -0,0 +1,419 @@
+package org.enso.runtime.parser.processor;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import org.enso.runtime.parser.dsl.IRChild;
+import org.enso.runtime.parser.dsl.IRCopyMethod;
+import org.enso.runtime.parser.dsl.IRNode;
+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.utils.Utils;
+
+/**
+ * Generates code for interfaces annotated with {@link org.enso.runtime.parser.dsl.IRNode}.
+ * Technically, the interface does not have to be annotated with {@link
+ * org.enso.runtime.parser.dsl.IRNode}, it can just be enclosed by another interface with that
+ * annotation.
+ *
+ * It is expected that the interface (passed as {@link javax.lang.model.element.TypeElement} in
+ * this class) extends {@link org.enso.compiler.core.IR}, either directly or via a hierarchy of
+ * other super interfaces.
+ *
+ *
Every parameterless abstract method defined by the interface (or any super interface) is
+ * treated as a field of the IR node. If the parameterless method is annotated with {@link
+ * org.enso.runtime.parser.dsl.IRChild}, it is treated as a child and will get into the
+ * generated code for, e.g., methods like {@link org.enso.compiler.core.IR#children()}.
+ */
+final class IRNodeClassGenerator {
+ private final ProcessingEnvironment processingEnv;
+ private final TypeElement interfaceType;
+
+ /** Name of the class that is being generated */
+ private final String className;
+
+ private final GeneratedClassContext generatedClassContext;
+ private final DuplicateMethodGenerator duplicateMethodGenerator;
+ private final SetLocationMethodGenerator setLocationMethodGenerator;
+ private final BuilderMethodGenerator builderMethodGenerator;
+ private final MapExpressionsMethodGenerator mapExpressionsMethodGenerator;
+ private final EqualsMethodGenerator equalsMethodGenerator;
+ private final HashCodeMethodGenerator hashCodeMethodGenerator;
+
+ /**
+ * For every method annotated with {@link IRCopyMethod}, there is a generator. Can be empty. Not
+ * null.
+ */
+ private final List copyMethodGenerators;
+
+ 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 interfaceType Type of the interface for which we are generating code. It is expected
+ * that the interface does not contain any nested interfaces or classes, just methods.
+ * @param className Name of the generated class. Non qualified.
+ */
+ IRNodeClassGenerator(
+ ProcessingEnvironment processingEnv, TypeElement interfaceType, String className) {
+ assert !className.contains(".") : "Class name should be simple, not qualified";
+ this.processingEnv = processingEnv;
+ this.interfaceType = interfaceType;
+ this.className = className;
+ var userFields = getAllUserFields(interfaceType);
+ var duplicateMethod = Utils.findDuplicateMethod(interfaceType, processingEnv);
+ this.generatedClassContext =
+ new GeneratedClassContext(className, userFields, processingEnv, interfaceType);
+ this.duplicateMethodGenerator =
+ new DuplicateMethodGenerator(duplicateMethod, generatedClassContext);
+ this.builderMethodGenerator = new BuilderMethodGenerator(generatedClassContext);
+ var mapExpressionsMethod = Utils.findMapExpressionsMethod(interfaceType, processingEnv);
+ this.mapExpressionsMethodGenerator =
+ new MapExpressionsMethodGenerator(mapExpressionsMethod, generatedClassContext);
+ var setLocationMethod =
+ Utils.findMethod(
+ interfaceType,
+ processingEnv,
+ method -> method.getSimpleName().toString().equals("setLocation"));
+ this.setLocationMethodGenerator =
+ new SetLocationMethodGenerator(setLocationMethod, processingEnv);
+ this.copyMethodGenerators =
+ findCopyMethods().stream()
+ .map(copyMethod -> new CopyMethodGenerator(copyMethod, generatedClassContext))
+ .toList();
+ this.equalsMethodGenerator = new EqualsMethodGenerator(generatedClassContext);
+ this.hashCodeMethodGenerator = new HashCodeMethodGenerator(generatedClassContext);
+ var nestedTypes =
+ interfaceType.getEnclosedElements().stream()
+ .filter(
+ elem ->
+ elem.getKind() == ElementKind.INTERFACE || elem.getKind() == ElementKind.CLASS)
+ .toList();
+ if (!nestedTypes.isEmpty()) {
+ throw new RuntimeException("Nested types must be handled separately: " + nestedTypes);
+ }
+ }
+
+ /**
+ * Finds all the methods annotated with {@link IRCopyMethod} in the interface hierarchy.
+ *
+ * @return empty if none. Not null.
+ */
+ private List findCopyMethods() {
+ var copyMethods = new ArrayList();
+ Utils.iterateSuperInterfaces(
+ interfaceType,
+ processingEnv,
+ (TypeElement iface) -> {
+ for (var enclosedElem : iface.getEnclosedElements()) {
+ if (enclosedElem instanceof ExecutableElement executableElem
+ && Utils.hasAnnotation(executableElem, IRCopyMethod.class)) {
+ copyMethods.add(executableElem);
+ }
+ }
+ return null;
+ });
+ return copyMethods;
+ }
+
+ /** Returns simple name of the generated class. */
+ String getClassName() {
+ return className;
+ }
+
+ /**
+ * Returns the simple name of the interface for which an implementing class is being generated.
+ */
+ String getInterfaceName() {
+ return interfaceType.getSimpleName().toString();
+ }
+
+ /** 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() {
+ return """
+ $fields
+
+ $constructor
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ $overrideUserDefinedMethods
+
+ $overrideIRMethods
+
+ $mapExpressionsMethod
+
+ $copyMethods
+
+ $equalsMethod
+
+ $hashCodeMethod
+
+ $builder
+ """
+ .replace("$fields", fieldsCode())
+ .replace("$constructor", constructor())
+ .replace("$overrideUserDefinedMethods", overrideUserDefinedMethods())
+ .replace("$overrideIRMethods", overrideIRMethods())
+ .replace("$mapExpressionsMethod", mapExpressions())
+ .replace("$copyMethods", copyMethods())
+ .replace("$equalsMethod", equalsMethodGenerator.generateMethodCode())
+ .replace("$hashCodeMethod", hashCodeMethodGenerator.generateMethodCode())
+ .replace("$builder", builderMethodGenerator.generateBuilder());
+ }
+
+ /**
+ * Collects all abstract methods (with no parameters) from this interface and all the interfaces
+ * that are extended by this interface. Every abstract method corresponds to a single field in the
+ * newly generated record. Abstract methods annotated with {@link IRChild} are considered IR
+ * children.
+ *
+ * @param irNodeInterface Type element of the interface annotated with {@link IRNode}.
+ * @return List of fields
+ */
+ private List getAllUserFields(TypeElement irNodeInterface) {
+ var fieldCollector = new FieldCollector(processingEnv, irNodeInterface);
+ 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;
+ // Not final on purpose
+ private DiagnosticStorage diagnostics;
+ private MetadataStorage passData;
+ private IdentifiedLocation location;
+ private UUID id;
+ """
+ .replace("$userDefinedFields", userDefinedFields);
+ return indent(code, 2);
+ }
+
+ /**
+ * Returns string representation of the package-private constructor of the generated class. Note
+ * that the constructor is meant to be invoked only by the internal Builder class.
+ */
+ private String constructor() {
+ var sb = new StringBuilder();
+ sb.append("private ").append(className).append("(");
+ var inParens =
+ generatedClassContext.getConstructorParameters().stream()
+ .map(
+ consParam ->
+ "$consType $consName"
+ .replace("$consType", consParam.type())
+ .replace("$consName", consParam.name()))
+ .collect(Collectors.joining(", "));
+ sb.append(inParens).append(") {").append(System.lineSeparator());
+ var ctorBody =
+ generatedClassContext.getAllFields().stream()
+ .map(field -> " this.$fieldName = $fieldName;".replace("$fieldName", field.name()))
+ .collect(Collectors.joining(System.lineSeparator()));
+ sb.append(indent(ctorBody, 2));
+ sb.append(System.lineSeparator());
+ sb.append("}").append(System.lineSeparator());
+ return indent(sb.toString(), 2);
+ }
+
+ 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 = "list.add(" + childField.getName() + ");";
+ } else {
+ addToListCode =
+ """
+ $childName.foreach(list::add);
+ """
+ .replace("$childName", 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);
+ }
+ });
+ 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 {@link org.enso.compiler.core.IR}.
+ * Meant to be inside the generated record definition.
+ */
+ private String overrideIRMethods() {
+ var code =
+ """
+
+ @Override
+ public MetadataStorage passData() {
+ if (passData == null) {
+ passData = new MetadataStorage();
+ }
+ 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$.empty();
+ }
+ return diagnostics;
+ }
+
+ $duplicateMethod
+
+ @Override
+ public String showCode(int indent) {
+ throw new UnsupportedOperationException("unimplemented");
+ }
+ """
+ .replace("$childrenMethodBody", childrenMethodBody())
+ .replace("$setLocationMethod", setLocationMethodGenerator.generateMethodCode())
+ .replace("$duplicateMethod", duplicateMethodGenerator.generateDuplicateMethodCode());
+ return indent(code, 2);
+ }
+
+ /**
+ * Returns string representation of all parameterless abstract methods from the interface
+ * annotated with {@link IRNode}.
+ *
+ * @return Code of the overriden methods
+ */
+ private String overrideUserDefinedMethods() {
+ var code =
+ generatedClassContext.getUserFields().stream()
+ .map(
+ field ->
+ """
+ @Override
+ public $returnType $fieldName() {
+ return $fieldName;
+ }
+ """
+ .replace("$returnType", field.getSimpleTypeName())
+ .replace("$fieldName", field.getName()))
+ .collect(Collectors.joining(System.lineSeparator()));
+ return indent(code, 2);
+ }
+
+ /**
+ * Generates the code for all the copy methods. Returns an empty string if there are no methods
+ * annotated with {@link IRCopyMethod}.
+ *
+ * @return Code of the copy method or an empty string if the method is not present.
+ */
+ private String copyMethods() {
+ return copyMethodGenerators.stream()
+ .map(CopyMethodGenerator::generateCopyMethod)
+ .collect(Collectors.joining(System.lineSeparator()));
+ }
+
+ private String mapExpressions() {
+ return Utils.indent(mapExpressionsMethodGenerator.generateMapExpressionsMethodCode(), 2);
+ }
+
+ 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/IRProcessor.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/IRProcessor.java
new file mode 100644
index 000000000000..8080988ba7a6
--- /dev/null
+++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/IRProcessor.java
@@ -0,0 +1,257 @@
+package org.enso.runtime.parser.processor;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+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.TypeElement;
+import javax.lang.model.util.SimpleElementVisitor14;
+import javax.tools.JavaFileObject;
+import org.enso.runtime.parser.dsl.IRNode;
+import org.enso.runtime.parser.processor.utils.Utils;
+
+@SupportedAnnotationTypes({
+ "org.enso.runtime.parser.dsl.IRNode",
+ "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 irNodeElems = roundEnv.getElementsAnnotatedWith(IRNode.class);
+ for (var irNodeElem : irNodeElems) {
+ var suc = processIrNode(irNodeElem);
+ if (!suc) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean processIrNode(Element irNodeElem) {
+ if (irNodeElem.getKind() != ElementKind.INTERFACE) {
+ printError("IRNode annotation can only be applied to interfaces", irNodeElem);
+ return false;
+ }
+ assert irNodeElem instanceof TypeElement;
+ var irNodeTypeElem = (TypeElement) irNodeElem;
+ if (!ensureExtendsSingleIRInterface(irNodeTypeElem)) {
+ return false;
+ }
+ var enclosingElem = irNodeElem.getEnclosingElement();
+ if (enclosingElem != null && enclosingElem.getKind() != ElementKind.PACKAGE) {
+ printError("Interface annotated with @IRNode must not be nested", irNodeElem);
+ return false;
+ }
+ var nestedInterfaces = collectNestedInterfaces(irNodeTypeElem);
+ var irNodeInterfaceName = irNodeTypeElem.getSimpleName().toString();
+ var pkgName = packageName(irNodeTypeElem);
+ var newClassName = irNodeInterfaceName + "Gen";
+ String newBinaryName;
+ if (!pkgName.isEmpty()) {
+ newBinaryName = pkgName + "." + newClassName;
+ } else {
+ newBinaryName = newClassName;
+ }
+
+ JavaFileObject srcGen = null;
+ try {
+ srcGen = processingEnv.getFiler().createSourceFile(newBinaryName, irNodeElem);
+ } catch (IOException e) {
+ printError("Failed to create source file for IRNode", irNodeElem);
+ }
+ assert srcGen != null;
+
+ String generatedCode;
+ if (nestedInterfaces.isEmpty()) {
+ var classGenerator = new IRNodeClassGenerator(processingEnv, irNodeTypeElem, newClassName);
+ generatedCode = generateSingleNodeClass(classGenerator, pkgName);
+ } else {
+ var nestedClassGenerators =
+ nestedInterfaces.stream()
+ .map(
+ iface -> {
+ var newNestedClassName = iface.getSimpleName().toString() + "Gen";
+ return new IRNodeClassGenerator(processingEnv, iface, newNestedClassName);
+ })
+ .toList();
+ generatedCode =
+ generateMultipleNodeClasses(
+ nestedClassGenerators, pkgName, newClassName, irNodeInterfaceName);
+ }
+
+ try {
+ try (var lineWriter = new PrintWriter(srcGen.openWriter())) {
+ lineWriter.write(generatedCode);
+ }
+ } catch (IOException e) {
+ printError("Failed to write to source file for IRNode", irNodeElem);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * The interface that is being processed must extend just a single interface that is a subtype of
+ * {@code org.enso.compiler.core.IR}. Otherwise, the code generation would not work correctly as
+ * it would find ambiguous methods to override.
+ *
+ * @param interfaceType The current interface annotated with {@link IRNode} being processed.
+ * @return {@code true} if the interface extends a single IR interface, {@code false} otherwise.
+ */
+ private boolean ensureExtendsSingleIRInterface(TypeElement interfaceType) {
+ var superIfacesExtendingIr =
+ interfaceType.getInterfaces().stream()
+ .filter(
+ superInterface -> {
+ var superInterfaceType =
+ (TypeElement) processingEnv.getTypeUtils().asElement(superInterface);
+ return Utils.isSubtypeOfIR(superInterfaceType, processingEnv);
+ })
+ .toList();
+ if (superIfacesExtendingIr.size() != 1) {
+ printError(
+ "Interface annotated with @IRNode must be a subtype of IR interface, "
+ + "and must extend only a single IR interface. All the IR interfaces found: "
+ + superIfacesExtendingIr,
+ interfaceType);
+ return false;
+ }
+ return true;
+ }
+
+ private String packageName(Element elem) {
+ var pkg = processingEnv.getElementUtils().getPackageOf(elem);
+ return pkg.getQualifiedName().toString();
+ }
+
+ private void printError(String msg, Element elem) {
+ Utils.printError(msg, elem, processingEnv.getMessager());
+ }
+
+ /**
+ * Generates code for a single class that implements a single interface annotated with {@link
+ * IRNode}.
+ *
+ * @param pkgName Package of the current interface annotated with {@link IRNode}.
+ * @return The generated code ready to be written to a {@code .java} source.
+ */
+ private static String generateSingleNodeClass(
+ IRNodeClassGenerator irNodeClassGen, String pkgName) {
+ var imports =
+ irNodeClassGen.imports().stream().collect(Collectors.joining(System.lineSeparator()));
+ var pkg = pkgName.isEmpty() ? "" : "package " + pkgName + ";";
+ var code =
+ """
+ $pkg
+
+ $imports
+
+ public final class $className implements $interfaceName {
+ $classBody
+ }
+ """
+ .replace("$pkg", pkg)
+ .replace("$imports", imports)
+ .replace("$className", irNodeClassGen.getClassName())
+ .replace("$interfaceName", irNodeClassGen.getInterfaceName())
+ .replace("$classBody", irNodeClassGen.classBody());
+ return code;
+ }
+
+ /**
+ * Generates code for many inner classes. This is the case when an outer interface annotated with
+ * {@link IRNode} contains many nested interfaces.
+ *
+ * @param nestedClassGenerators Class generators for all the nested interfaces.
+ * @param pkgName Package of the outer interface annotated with {@link IRNode}.
+ * @param newOuterClassName Name for the newly generate public outer class.
+ * @param outerInterfaceName Name of the interface annotated by {@link IRNode}, that is, the outer
+ * interface for which we are generating multiple inner classes.
+ * @return The generated code ready to be written to a {@code .java} source.
+ */
+ private static String generateMultipleNodeClasses(
+ List nestedClassGenerators,
+ String pkgName,
+ String newOuterClassName,
+ String outerInterfaceName) {
+ var imports =
+ nestedClassGenerators.stream()
+ .flatMap(gen -> gen.imports().stream())
+ .collect(Collectors.joining(System.lineSeparator()));
+ var sb = new StringBuilder();
+ if (!pkgName.isEmpty()) {
+ sb.append("package ").append(pkgName).append(";").append(System.lineSeparator());
+ }
+ sb.append(imports);
+ sb.append(System.lineSeparator());
+ sb.append(System.lineSeparator());
+ sb.append("public final class ")
+ .append(newOuterClassName)
+ .append(" {")
+ .append(System.lineSeparator());
+ sb.append(System.lineSeparator());
+ sb.append(" ")
+ .append("private ")
+ .append(newOuterClassName)
+ .append("() {}")
+ .append(System.lineSeparator());
+ sb.append(System.lineSeparator());
+ for (var classGen : nestedClassGenerators) {
+ sb.append(" public static final class ")
+ .append(classGen.getClassName())
+ .append(" implements ")
+ .append(outerInterfaceName)
+ .append(".")
+ .append(classGen.getInterfaceName())
+ .append(" {")
+ .append(System.lineSeparator());
+ sb.append(Utils.indent(classGen.classBody(), 2));
+ sb.append(" }");
+ sb.append(System.lineSeparator());
+ }
+ sb.append("}");
+ sb.append(System.lineSeparator());
+ return sb.toString();
+ }
+
+ private List collectNestedInterfaces(TypeElement interfaceType) {
+ var nestedTypes = new ArrayList();
+ var typeVisitor =
+ new SimpleElementVisitor14() {
+ @Override
+ protected Void defaultAction(Element e, Void unused) {
+ for (var childElem : e.getEnclosedElements()) {
+ childElem.accept(this, unused);
+ }
+ return null;
+ }
+
+ @Override
+ public Void visitType(TypeElement e, Void unused) {
+ if (e.getKind() == ElementKind.INTERFACE) {
+ nestedTypes.add(e);
+ }
+ return super.visitType(e, unused);
+ }
+ };
+ for (var enclosedElem : interfaceType.getEnclosedElements()) {
+ enclosedElem.accept(typeVisitor, null);
+ }
+ return nestedTypes;
+ }
+}
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..b5d1277ebf3c
--- /dev/null
+++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/Field.java
@@ -0,0 +1,71 @@
+package org.enso.runtime.parser.processor.field;
+
+import java.util.List;
+import java.util.function.Function;
+import javax.lang.model.element.TypeElement;
+import org.enso.runtime.parser.dsl.IRChild;
+
+/**
+ * A field of an IR node. Represented by any parameterless method on an interface annotated with
+ * {@link org.enso.runtime.parser.dsl.IRNode}.
+ */
+public interface Field {
+
+ /** Name (identifier) of the field. */
+ String getName();
+
+ /** Returns type of this field. Null if this is a primitive field. */
+ TypeElement getType();
+
+ /**
+ * Does not return null. If the type is generic, the type parameter is included in the name.
+ * Returns non-qualified name.
+ */
+ String getSimpleTypeName();
+
+ /**
+ * Returns list of (fully-qualified) types that are necessary to import in order to use simple
+ * type names.
+ */
+ default List getImportedTypes() {
+ return List.of();
+ }
+
+ /** Returns true if this field is a scala immutable list. */
+ default boolean isList() {
+ return false;
+ }
+
+ /**
+ * Returns true if this field is annotated with {@link org.enso.runtime.parser.dsl.IRChild}.
+ *
+ * @return
+ */
+ boolean isChild();
+
+ /**
+ * Returns true if this field is child with {@link IRChild#required()} set to false.
+ *
+ * @return
+ */
+ boolean isNullable();
+
+ /** Returns true if the type of this field is Java primitive. */
+ boolean isPrimitive();
+
+ /**
+ * Returns true if this field extends {@link org.enso.compiler.core.ir.Expression} ({@link
+ * org.enso.compiler.core.ir.JExpression}).
+ *
+ * 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}
+ */
+ boolean isExpression();
+
+ /** Returns the type parameter, if this field is a generic type. Otherwise null. */
+ default String getTypeParameter() {
+ 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..5d3c798e2b8a
--- /dev/null
+++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/FieldCollector.java
@@ -0,0 +1,117 @@
+package org.enso.runtime.parser.processor.field;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.processing.ProcessingEnvironment;
+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.TypeMirror;
+import org.enso.runtime.parser.dsl.IRChild;
+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 TypeElement irNodeInterface;
+ // Mapped by field name
+ private final Map fields = new LinkedHashMap<>();
+
+ /**
+ * @param irNodeInterface For this interface, fields will be collected.
+ */
+ public FieldCollector(ProcessingEnvironment processingEnv, TypeElement irNodeInterface) {
+ assert irNodeInterface.getKind() == ElementKind.INTERFACE;
+ this.processingEnv = processingEnv;
+ this.irNodeInterface = irNodeInterface;
+ }
+
+ public List collectFields() {
+ var superInterfaces = irNodeInterface.getInterfaces();
+ Deque toProcess = new ArrayDeque<>();
+ toProcess.add(irNodeInterface.asType());
+ toProcess.addAll(superInterfaces);
+ // Process transitively all the super interface until the parent IR is reached.
+ while (!toProcess.isEmpty()) {
+ var current = toProcess.pop();
+ // Skip processing of IR root interface.
+ if (Utils.isIRInterface(current, processingEnv)) {
+ continue;
+ }
+ var currentElem = processingEnv.getTypeUtils().asElement(current);
+ if (currentElem instanceof TypeElement currentTypeElem) {
+ collectFromSingleInterface(currentTypeElem);
+ // Add all super interfaces to the processing queue, if they are not there already.
+ for (var superInterface : currentTypeElem.getInterfaces()) {
+ if (!toProcess.contains(superInterface)) {
+ toProcess.add(superInterface);
+ }
+ }
+ }
+ }
+ return fields.values().stream().toList();
+ }
+
+ /** Collect only parameterless methods without default implementation. */
+ private void collectFromSingleInterface(TypeElement typeElem) {
+ assert typeElem.getKind() == ElementKind.INTERFACE;
+ for (var childElem : typeElem.getEnclosedElements()) {
+ if (childElem instanceof ExecutableElement methodElement) {
+ if (methodElement.getParameters().isEmpty()
+ && !Utils.hasImplementation(methodElement, irNodeInterface, processingEnv)) {
+ var name = methodElement.getSimpleName().toString();
+ if (!fields.containsKey(name)) {
+ var field = methodToField(methodElement);
+ fields.put(name, field);
+ }
+ }
+ }
+ }
+ }
+
+ private Field methodToField(ExecutableElement methodElement) {
+ var name = methodElement.getSimpleName().toString();
+ var retType = methodElement.getReturnType();
+ if (retType.getKind().isPrimitive()) {
+ return new PrimitiveField(retType, name);
+ }
+
+ var retTypeElem = (TypeElement) processingEnv.getTypeUtils().asElement(retType);
+ assert retTypeElem != null;
+ var childAnnot = methodElement.getAnnotation(IRChild.class);
+ if (childAnnot == null) {
+ return new ReferenceField(processingEnv, retTypeElem, name, false, false);
+ }
+
+ assert childAnnot != null;
+ if (Utils.isScalaList(retTypeElem, processingEnv)) {
+ assert retType instanceof DeclaredType;
+ var declaredRetType = (DeclaredType) retType;
+ assert declaredRetType.getTypeArguments().size() == 1;
+ var typeArg = declaredRetType.getTypeArguments().get(0);
+ var typeArgElem = (TypeElement) processingEnv.getTypeUtils().asElement(typeArg);
+ ensureIsSubtypeOfIR(typeArgElem);
+ return new ListField(name, retTypeElem, typeArgElem);
+ }
+
+ boolean isNullable = !childAnnot.required();
+ ensureIsSubtypeOfIR(retTypeElem);
+ return new ReferenceField(processingEnv, retTypeElem, name, isNullable, true);
+ }
+
+ private void ensureIsSubtypeOfIR(TypeElement typeElem) {
+ if (!Utils.isSubtypeOfIR(typeElem, processingEnv)) {
+ Utils.printError(
+ "Method annotated with @IRChild must return a subtype of IR interface",
+ typeElem,
+ processingEnv.getMessager());
+ }
+ }
+}
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..6942d19df2a5
--- /dev/null
+++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/ListField.java
@@ -0,0 +1,73 @@
+package org.enso.runtime.parser.processor.field;
+
+import java.util.List;
+import javax.lang.model.element.TypeElement;
+
+/** Represents a {@code scala.collection.immutable.List} field in the IR node. */
+final class ListField implements Field {
+ private final String name;
+ private final TypeElement typeArgElement;
+ private final TypeElement type;
+
+ /**
+ * @param name Name of the field
+ * @param typeArgElement TypeElement of the type argument. Must be subtype of IR.
+ */
+ ListField(String name, TypeElement type, TypeElement typeArgElement) {
+ this.name = name;
+ this.type = type;
+ this.typeArgElement = typeArgElement;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public TypeElement getType() {
+ return type;
+ }
+
+ @Override
+ public String getSimpleTypeName() {
+ var typeArg = typeArgElement.getSimpleName().toString();
+ return "List<" + typeArg + ">";
+ }
+
+ @Override
+ public String getTypeParameter() {
+ return typeArgElement.getSimpleName().toString();
+ }
+
+ @Override
+ public List getImportedTypes() {
+ var typePar = typeArgElement.getQualifiedName().toString();
+ return List.of("scala.collection.immutable.List", typePar);
+ }
+
+ @Override
+ public boolean isList() {
+ return true;
+ }
+
+ @Override
+ public boolean isChild() {
+ return true;
+ }
+
+ @Override
+ public boolean isNullable() {
+ return false;
+ }
+
+ @Override
+ public boolean isPrimitive() {
+ return false;
+ }
+
+ @Override
+ public boolean isExpression() {
+ 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..5bd90afb7c74
--- /dev/null
+++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/PrimitiveField.java
@@ -0,0 +1,50 @@
+package org.enso.runtime.parser.processor.field;
+
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+
+final class PrimitiveField implements Field {
+
+ private final TypeMirror type;
+ private final String name;
+
+ PrimitiveField(TypeMirror type, String name) {
+ this.type = type;
+ this.name = name;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public TypeElement getType() {
+ return null;
+ }
+
+ @Override
+ public String getSimpleTypeName() {
+ return type.toString();
+ }
+
+ @Override
+ public boolean isChild() {
+ return false;
+ }
+
+ @Override
+ public boolean isNullable() {
+ return false;
+ }
+
+ @Override
+ public boolean isPrimitive() {
+ return true;
+ }
+
+ @Override
+ public boolean isExpression() {
+ return false;
+ }
+}
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..ec40774422c3
--- /dev/null
+++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/field/ReferenceField.java
@@ -0,0 +1,67 @@
+package org.enso.runtime.parser.processor.field;
+
+import java.util.List;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.TypeElement;
+import org.enso.runtime.parser.processor.utils.Utils;
+
+final class ReferenceField implements Field {
+ private final ProcessingEnvironment procEnv;
+ private final TypeElement type;
+ private final String name;
+ private final boolean nullable;
+ private final boolean isChild;
+
+ ReferenceField(
+ ProcessingEnvironment procEnv,
+ TypeElement type,
+ String name,
+ boolean nullable,
+ boolean isChild) {
+ this.procEnv = procEnv;
+ this.type = type;
+ this.name = name;
+ this.nullable = nullable;
+ this.isChild = isChild;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public TypeElement getType() {
+ return type;
+ }
+
+ @Override
+ public String getSimpleTypeName() {
+ return type.getSimpleName().toString();
+ }
+
+ @Override
+ public List getImportedTypes() {
+ return List.of(type.getQualifiedName().toString());
+ }
+
+ @Override
+ public boolean isChild() {
+ return isChild;
+ }
+
+ @Override
+ public boolean isPrimitive() {
+ return false;
+ }
+
+ @Override
+ public boolean isNullable() {
+ return nullable;
+ }
+
+ @Override
+ public boolean isExpression() {
+ return Utils.isSubtypeOfExpression(type.asType(), procEnv);
+ }
+}
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..87117ebd4828
--- /dev/null
+++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/BuilderMethodGenerator.java
@@ -0,0 +1,115 @@
+package org.enso.runtime.parser.processor.methodgen;
+
+import java.util.stream.Collectors;
+import org.enso.runtime.parser.processor.GeneratedClassContext;
+import org.enso.runtime.parser.processor.GeneratedClassContext.ClassField;
+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(
+ metaField ->
+ """
+ private $type $name;
+ """
+ .replace("$type", metaField.type())
+ .replace("$name", metaField.name()))
+ .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.type()))
+ .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 fieldList =
+ generatedClassContext.getAllFields().stream()
+ .map(ClassField::name)
+ .collect(Collectors.joining(", "));
+
+ var code =
+ """
+ public static final class Builder {
+ $fieldDeclarations
+
+ Builder() {}
+
+ $copyConstructor
+
+ $fieldSetters
+
+ public $className build() {
+ validate();
+ return new $className($fieldList);
+ }
+
+ private void validate() {
+ $validationCode
+ }
+ }
+ """
+ .replace("$fieldDeclarations", fieldDeclarations)
+ .replace("$copyConstructor", copyConstructor())
+ .replace("$fieldSetters", fieldSetters)
+ .replace("$className", generatedClassContext.getClassName())
+ .replace("$fieldList", fieldList)
+ .replace("$validationCode", Utils.indent(validationCode, 2));
+ return Utils.indent(code, 2);
+ }
+
+ private String copyConstructor() {
+ var sb = new StringBuilder();
+ sb.append("/** Copy constructor */").append(System.lineSeparator());
+ sb.append("Builder(")
+ .append(generatedClassContext.getClassName())
+ .append(" from) {")
+ .append(System.lineSeparator());
+ for (var classField : generatedClassContext.getAllFields()) {
+ sb.append(" this.")
+ .append(classField.name())
+ .append(" = from.")
+ .append(classField.name())
+ .append(";")
+ .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..098c49867662
--- /dev/null
+++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/CopyMethodGenerator.java
@@ -0,0 +1,97 @@
+package org.enso.runtime.parser.processor.methodgen;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.VariableElement;
+import org.enso.runtime.parser.dsl.IRCopyMethod;
+import org.enso.runtime.parser.processor.GeneratedClassContext;
+import org.enso.runtime.parser.processor.GeneratedClassContext.ClassField;
+import org.enso.runtime.parser.processor.utils.Utils;
+
+/** Code generator for methods annotated with {@link IRCopyMethod}. */
+public class CopyMethodGenerator {
+ private final ExecutableElement copyMethod;
+ private final GeneratedClassContext ctx;
+ private final Map parameterMapping = new HashMap<>();
+
+ public CopyMethodGenerator(ExecutableElement copyMethod, GeneratedClassContext ctx) {
+ this.ctx = ctx;
+ ensureIsAnnotated(copyMethod);
+ this.copyMethod = Objects.requireNonNull(copyMethod);
+ for (var parameter : copyMethod.getParameters()) {
+ var parName = parameter.getSimpleName();
+ var parType = simpleTypeName(parameter);
+ ctx.getAllFields().stream()
+ .filter(field -> field.type().equals(parType))
+ .filter(field -> field.name().equals(parName.toString()))
+ .findFirst()
+ .ifPresentOrElse(
+ field -> parameterMapping.put(parameter, field),
+ () -> {
+ var msg =
+ "Parameter "
+ + parName
+ + " of type "
+ + parType
+ + " in the copy method "
+ + "does not have a corresponding field in the interface "
+ + ctx.getIrNodeInterface().getQualifiedName().toString()
+ + ". Ensure there is a parameterless abstract method of the same return"
+ + " type. For more information, see @IRNode annotation docs.";
+ Utils.printError(msg, parameter, ctx.getProcessingEnvironment().getMessager());
+ throw new IllegalArgumentException(msg);
+ });
+ }
+ Utils.hardAssert(parameterMapping.size() == copyMethod.getParameters().size());
+ }
+
+ private static void ensureIsAnnotated(ExecutableElement copyMethod) {
+ if (copyMethod.getAnnotation(IRCopyMethod.class) == null) {
+ throw new IllegalArgumentException("Copy method must be annotated with @IRCopyMethod");
+ }
+ }
+
+ private String simpleTypeName(VariableElement parameter) {
+ return ctx.getProcessingEnvironment()
+ .getTypeUtils()
+ .asElement(parameter.asType())
+ .getSimpleName()
+ .toString();
+ }
+
+ public String generateCopyMethod() {
+ var sb = new StringBuilder();
+ sb.append("@Override").append(System.lineSeparator());
+ var argList =
+ copyMethod.getParameters().stream()
+ .map(
+ parameter -> simpleTypeName(parameter) + " " + parameter.getSimpleName().toString())
+ .collect(Collectors.joining(", "));
+ sb.append("public ")
+ .append(copyMethod.getReturnType())
+ .append(" ")
+ .append(copyMethod.getSimpleName())
+ .append("(")
+ .append(argList)
+ .append(") {")
+ .append(System.lineSeparator());
+ sb.append(" var builder = new Builder(this);").append(System.lineSeparator());
+ for (var entry : parameterMapping.entrySet()) {
+ var parameter = entry.getKey();
+ var classField = entry.getValue();
+ sb.append(" builder.")
+ .append(classField.name())
+ .append(" = ")
+ .append(parameter.getSimpleName())
+ .append(";")
+ .append(System.lineSeparator());
+ }
+ sb.append(" var ret = builder.build();").append(System.lineSeparator());
+ sb.append(" return ret;").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/DuplicateMethodGenerator.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/DuplicateMethodGenerator.java
new file mode 100644
index 000000000000..4acd6073f25d
--- /dev/null
+++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/DuplicateMethodGenerator.java
@@ -0,0 +1,246 @@
+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 org.enso.runtime.parser.processor.GeneratedClassContext;
+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 ExecutableElement duplicateMethod;
+ private final GeneratedClassContext ctx;
+ private static final List parameters =
+ List.of(
+ new Parameter("boolean", "keepLocations"),
+ new Parameter("boolean", "keepMetadata"),
+ new Parameter("boolean", "keepDiagnostics"),
+ new Parameter("boolean", "keepIdentifiers"));
+
+ /**
+ * @param duplicateMethod ExecutableElement representing the duplicate method (or its override).
+ */
+ public DuplicateMethodGenerator(ExecutableElement duplicateMethod, GeneratedClassContext ctx) {
+ ensureDuplicateMethodHasExpectedSignature(duplicateMethod);
+ this.ctx = Objects.requireNonNull(ctx);
+ this.duplicateMethod = Objects.requireNonNull(duplicateMethod);
+ }
+
+ private static void ensureDuplicateMethodHasExpectedSignature(ExecutableElement duplicateMethod) {
+ var dupMethodParameters = duplicateMethod.getParameters();
+ if (dupMethodParameters.size() != parameters.size()) {
+ throw new IllegalArgumentException(
+ "Duplicate method must have " + parameters.size() + " parameters");
+ }
+ var allParamsAreBooleans =
+ dupMethodParameters.stream().allMatch(par -> par.asType().getKind() == TypeKind.BOOLEAN);
+ if (!allParamsAreBooleans) {
+ throw new IllegalArgumentException(
+ "All parameters of the duplicate method must be of type boolean");
+ }
+ }
+
+ public String generateDuplicateMethodCode() {
+ 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;
+ if (keepDiagnostics) {
+ diagnosticsDuplicated = this.diagnostics;
+ } else {
+ diagnosticsDuplicated = null;
+ }
+ $metaType passDataDuplicated;
+ if (keepMetadata) {
+ passDataDuplicated = this.passData;
+ } else {
+ passDataDuplicated = null;
+ }
+ $locType locationDuplicated;
+ if (keepLocations) {
+ locationDuplicated = this.location;
+ } else {
+ locationDuplicated = null;
+ }
+ $idType idDuplicated;
+ if (keepIdentifiers) {
+ idDuplicated = this.id;
+ } else {
+ idDuplicated = null;
+ }
+ """
+ .replace("$locType", ctx.getLocationMetaField().type())
+ .replace("$metaType", ctx.getPassDataMetaField().type())
+ .replace("$diagType", ctx.getDiagnosticsMetaField().type())
+ .replace("$idType", ctx.getIdMetaField().type());
+ sb.append(Utils.indent(duplicateMetaFieldsCode, 2));
+ sb.append(System.lineSeparator());
+ for (var dupMetaVarName :
+ List.of(
+ "diagnosticsDuplicated", "passDataDuplicated", "locationDuplicated", "idDuplicated")) {
+ duplicatedVars.add(new DuplicateVar(null, dupMetaVarName, 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.getSimpleTypeName(), dupFieldName(field), true));
+ } else if (!field.isNullable() && !field.isList()) {
+ sb.append(Utils.indent(notNullableChildCode(field), 2));
+ sb.append(System.lineSeparator());
+ duplicatedVars.add(
+ new DuplicateVar(field.getSimpleTypeName(), dupFieldName(field), true));
+ } else if (field.isList()) {
+ sb.append(Utils.indent(listChildCode(field), 2));
+ sb.append(System.lineSeparator());
+ duplicatedVars.add(new DuplicateVar(null, dupFieldName(field), false));
+ }
+ } else {
+ sb.append(Utils.indent(nonChildCode(field), 2));
+ sb.append(System.lineSeparator());
+ duplicatedVars.add(new DuplicateVar(field.getSimpleTypeName(), dupFieldName(field), false));
+ }
+ }
+
+ sb.append(Utils.indent(returnStatement(duplicatedVars), 2));
+ sb.append(System.lineSeparator());
+ sb.append("}");
+ sb.append(System.lineSeparator());
+ return sb.toString();
+ }
+
+ private static String dupFieldName(Field field) {
+ return field.getName() + "Duplicated";
+ }
+
+ private static String nullableChildCode(Field nullableChild) {
+ assert 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 static String notNullableChildCode(Field child) {
+ assert child.isChild() && !child.isNullable() && !child.isList();
+ 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 static String listChildCode(Field listChild) {
+ assert 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())
+ .replace("$childName", listChild.getName())
+ .replace("$dupName", dupFieldName(listChild))
+ .replace("$parameterNames", String.join(", ", parameterNames()));
+ }
+
+ private static String nonChildCode(Field field) {
+ assert !field.isChild();
+ return """
+ $childType $dupName = $childName;
+ """
+ .replace("$childType", field.getSimpleTypeName())
+ .replace("$childName", field.getName())
+ .replace("$dupName", dupFieldName(field));
+ }
+
+ private static List parameterNames() {
+ return parameters.stream().map(Parameter::name).collect(Collectors.toList());
+ }
+
+ private String returnStatement(List duplicatedVars) {
+ Utils.hardAssert(
+ duplicatedVars.size() == ctx.getConstructorParameters().size(),
+ "Number of duplicated variables must be equal to the number of constructor parameters");
+ var argList =
+ duplicatedVars.stream()
+ .map(
+ var -> {
+ if (var.needsCast) {
+ return "(" + var.type + ") " + var.name;
+ } else {
+ return var.name;
+ }
+ })
+ .collect(Collectors.joining(", "));
+ return "return new " + ctx.getClassName() + "(" + argList + ");";
+ }
+
+ private String dupMethodRetType() {
+ return duplicateMethod.getReturnType().toString();
+ }
+
+ private static String stripWhitespaces(String s) {
+ return s.replaceAll("\\s+", "");
+ }
+
+ /**
+ * @param type Nullable
+ * @param name
+ * @param needsCast If the duplicated variable needs to be casted to its type in the return
+ * statement.
+ */
+ private record DuplicateVar(String type, String name, boolean needsCast) {}
+
+ /**
+ * Parameter for the duplicate method
+ *
+ * @param type
+ * @param name
+ */
+ private record Parameter(String type, String name) {
+
+ @Override
+ public String toString() {
+ return type + " " + 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..c8319ec0216c
--- /dev/null
+++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/EqualsMethodGenerator.java
@@ -0,0 +1,38 @@
+package org.enso.runtime.parser.processor.methodgen;
+
+import org.enso.runtime.parser.processor.GeneratedClassContext;
+import org.enso.runtime.parser.processor.utils.Utils;
+
+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 Utils.indent(sb.toString(), 2);
+ }
+}
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..a9463d2ed8ce
--- /dev/null
+++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/HashCodeMethodGenerator.java
@@ -0,0 +1,28 @@
+package org.enso.runtime.parser.processor.methodgen;
+
+import java.util.stream.Collectors;
+import org.enso.runtime.parser.processor.GeneratedClassContext;
+import org.enso.runtime.parser.processor.GeneratedClassContext.ClassField;
+import org.enso.runtime.parser.processor.utils.Utils;
+
+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 Utils.indent(code, 2);
+ }
+}
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..64c972afbd09
--- /dev/null
+++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/MapExpressionsMethodGenerator.java
@@ -0,0 +1,145 @@
+package org.enso.runtime.parser.processor.methodgen;
+
+import java.util.Objects;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import org.enso.runtime.parser.processor.GeneratedClassContext;
+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) {
+ Utils.printErrorAndFail(
+ "Map expressions method must have 1 parameter",
+ mapExpressionsMethod,
+ ctx.getProcessingEnvironment().getMessager());
+ }
+ }
+
+ public String generateMapExpressionsMethodCode() {
+ var sb = new StringBuilder();
+ sb.append("@Override").append(System.lineSeparator());
+ sb.append("public ")
+ .append(mapExpressionsMethod.getReturnType())
+ .append(" ")
+ .append(METHOD_NAME)
+ .append("(")
+ .append("Function fn")
+ .append(") {")
+ .append(System.lineSeparator());
+
+ var children = ctx.getUserFields().stream().filter(field -> field.isChild() && !field.isList());
+ var newChildren =
+ children.map(
+ child -> {
+ Utils.hardAssert(!child.isList());
+ var childsMapExprMethod =
+ Utils.findMapExpressionsMethod(child.getType(), ctx.getProcessingEnvironment());
+ var typeUtils = ctx.getProcessingEnvironment().getTypeUtils();
+ var childsMapExprMethodRetType =
+ typeUtils.asElement(childsMapExprMethod.getReturnType());
+ var shouldCast =
+ !typeUtils.isSameType(
+ child.getType().asType(), childsMapExprMethodRetType.asType());
+
+ var newChildName = child.getName() + "Mapped";
+ sb.append(" ")
+ .append(typeName(childsMapExprMethodRetType))
+ .append(" ")
+ .append(newChildName);
+ if (child.isNullable()) {
+ sb.append(" = null;").append(System.lineSeparator());
+ sb.append(" if (")
+ .append(child.getName())
+ .append(" != null) {")
+ .append(System.lineSeparator());
+ // 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()) {
+ // 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.isNullable());
+ // List childMapped = child.map(e -> e.mapExpressions(fn));
+ sb.append(" = ")
+ .append(child.getName())
+ .append(".map(e -> e.")
+ .append(METHOD_NAME)
+ .append("(fn));")
+ .append(System.lineSeparator());
+ }
+ }
+ return new MappedChild(newChildName, child, shouldCast);
+ });
+ sb.append(" ").append("var bldr = new Builder(this);").append(System.lineSeparator());
+ newChildren.forEach(
+ newChild -> {
+ if (newChild.shouldCast) {
+ sb.append(" ")
+ .append("if (!(")
+ .append(newChild.newChildName)
+ .append(" instanceof ")
+ .append(newChild.child.getType().getSimpleName())
+ .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.getType().getSimpleName()).append(") ");
+ }
+ sb.append(newChild.newChildName).append(");").append(System.lineSeparator());
+ });
+ sb.append(" return bldr.build();").append(System.lineSeparator());
+ sb.append("}").append(System.lineSeparator());
+ return sb.toString();
+ }
+
+ private String typeName(Element element) {
+ if (element instanceof TypeElement typeElement) {
+ return typeElement.getQualifiedName().toString();
+ }
+ return element.getSimpleName().toString();
+ }
+
+ 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..3efd3d0ad955
--- /dev/null
+++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/methodgen/SetLocationMethodGenerator.java
@@ -0,0 +1,53 @@
+package org.enso.runtime.parser.processor.methodgen;
+
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.ExecutableElement;
+import org.enso.runtime.parser.processor.utils.Utils;
+
+public class SetLocationMethodGenerator {
+ private final ExecutableElement setLocationMethod;
+ private final ProcessingEnvironment processingEnv;
+
+ public SetLocationMethodGenerator(
+ ExecutableElement setLocationMethod, ProcessingEnvironment processingEnv) {
+ ensureCorrectSignature(setLocationMethod);
+ this.processingEnv = processingEnv;
+ this.setLocationMethod = setLocationMethod;
+ }
+
+ private static void ensureCorrectSignature(ExecutableElement setLocationMethod) {
+ if (!setLocationMethod.getSimpleName().toString().equals("setLocation")) {
+ throw new IllegalArgumentException(
+ "setLocation method must be named setLocation, but was: " + setLocationMethod);
+ }
+ if (setLocationMethod.getParameters().size() != 1) {
+ throw new IllegalArgumentException(
+ "setLocation method must have exactly one parameter, but had: "
+ + setLocationMethod.getParameters());
+ }
+ }
+
+ 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 Utils.indent(code, 2);
+ }
+
+ private String retType() {
+ return processingEnv
+ .getTypeUtils()
+ .asElement(setLocationMethod.getReturnType())
+ .getSimpleName()
+ .toString();
+ }
+}
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..c6a887592a1c
--- /dev/null
+++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/utils/Utils.java
@@ -0,0 +1,231 @@
+package org.enso.runtime.parser.processor.utils;
+
+import java.lang.annotation.Annotation;
+import java.util.ArrayDeque;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import javax.annotation.processing.Messager;
+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.Modifier;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+import javax.tools.Diagnostic.Kind;
+
+public final class Utils {
+
+ private static final String MAP_EXPRESSIONS = "mapExpressions";
+ private static final String DUPLICATE = "duplicate";
+
+ 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")) {
+ 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");
+ }
+
+ /** Returns true if the given type extends {@link org.enso.compiler.core.ir.Expression} */
+ public static boolean isSubtypeOfExpression(
+ TypeMirror type, ProcessingEnvironment processingEnv) {
+ var expressionType =
+ processingEnv
+ .getElementUtils()
+ .getTypeElement("org.enso.compiler.core.ir.Expression")
+ .asType();
+ return processingEnv.getTypeUtils().isAssignable(type, expressionType);
+ }
+
+ public static void printError(String msg, Element elem, Messager messager) {
+ messager.printMessage(Kind.ERROR, msg, elem);
+ }
+
+ public static void printErrorAndFail(String msg, Element elem, Messager messager) {
+ printError(msg, elem, messager);
+ throw new IllegalStateException("Unexpected failure during annotation processing: " + msg);
+ }
+
+ public static String indent(String code, int indentation) {
+ return code.lines()
+ .map(line -> " ".repeat(indentation) + line)
+ .collect(Collectors.joining(System.lineSeparator()));
+ }
+
+ public static boolean isScalaList(TypeElement type, ProcessingEnvironment procEnv) {
+ var scalaListType = procEnv.getElementUtils().getTypeElement("scala.collection.immutable.List");
+ return procEnv.getTypeUtils().isAssignable(type.asType(), scalaListType.asType());
+ }
+
+ /**
+ * Returns true if the method has an implementation (is default or static) in some of the super
+ * interfaces.
+ *
+ * If the method is implemented in some of the super interfaces, there must not be generated an
+ * override for it - that would result in compilation error.
+ *
+ * @param method the method to check
+ * @param interfaceType the interface that declares the method to check for the implementation.
+ * @param procEnv
+ * @return
+ */
+ public static boolean hasImplementation(
+ ExecutableElement method, TypeElement interfaceType, ProcessingEnvironment procEnv) {
+ var defImplFound =
+ iterateSuperInterfaces(
+ interfaceType,
+ procEnv,
+ (TypeElement superInterface) -> {
+ for (var enclosedElem : superInterface.getEnclosedElements()) {
+ if (enclosedElem instanceof ExecutableElement executableElem) {
+ if (executableElem.getSimpleName().equals(method.getSimpleName())) {
+ if (hasModifier(executableElem, Modifier.DEFAULT)
+ || hasModifier(executableElem, Modifier.STATIC)) {
+ return true;
+ }
+ }
+ }
+ }
+ return null;
+ });
+ return defImplFound != null;
+ }
+
+ static boolean hasModifier(ExecutableElement method, Modifier modifier) {
+ return method.getModifiers().contains(modifier);
+ }
+
+ /**
+ * 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 AssertionError(msg);
+ }
+ }
+
+ 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;
+ }
+}
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/TreeToIr.java b/engine/runtime-parser/src/main/java/org/enso/compiler/core/TreeToIr.java
index 14e2fd759503..6bfd3706dcad 100644
--- a/engine/runtime-parser/src/main/java/org/enso/compiler/core/TreeToIr.java
+++ b/engine/runtime-parser/src/main/java/org/enso/compiler/core/TreeToIr.java
@@ -492,7 +492,7 @@ private List translateMethodBinding(Tree.Function fn, List