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: + * + * + * + * 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 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 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