diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/Field.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/Field.java new file mode 100644 index 000000000000..953316ac2c2c --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/Field.java @@ -0,0 +1,42 @@ +package org.enso.runtime.parser.processor; + +import javax.lang.model.element.TypeElement; + +final class Field { + private final TypeElement type; + + /** Name of the field (identifier). */ + private final String name; + + /** If the field can be {@code null}. */ + private final boolean nullable; + + private final boolean isChild; + + Field(TypeElement type, String name, boolean nullable, boolean isChild) { + this.type = type; + this.name = name; + this.nullable = nullable; + this.isChild = isChild; + } + + boolean isChild() { + return isChild; + } + + boolean isNullable() { + return nullable; + } + + String getName() { + return name; + } + + String getSimpleTypeName() { + return type.getSimpleName().toString(); + } + + String getQualifiedTypeName() { + return type.getQualifiedName().toString(); + } +} diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/IRNodeElement.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/IRNodeElement.java index c88e04c8b8f2..bc13bea71da5 100644 --- a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/IRNodeElement.java +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/IRNodeElement.java @@ -1,8 +1,15 @@ package org.enso.runtime.parser.processor; +import java.util.ArrayList; +import java.util.List; import java.util.stream.Collectors; import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; +import javax.lang.model.util.SimpleElementVisitor14; +import org.enso.runtime.parser.dsl.IRChild; +import org.enso.runtime.parser.dsl.IRNode; /** * Representation of an interface annotated with {@link org.enso.runtime.parser.dsl.IRNode}. Takes @@ -12,8 +19,10 @@ */ final class IRNodeElement { private final ProcessingEnvironment processingEnv; + private final String recordName; + private final List fields; - static final String IMPORTS = + private static final String IMPORTS = """ import java.util.UUID; import java.util.function.Function; @@ -27,9 +36,102 @@ final class IRNodeElement { import scala.collection.immutable.List; """; - IRNodeElement(ProcessingEnvironment processingEnv, TypeElement irNodeInterface) { + /** + * @param processingEnv + * @param irNodeInterface + * @param recordName Simple name (non-qualified) of the newly generated record. + */ + IRNodeElement( + ProcessingEnvironment processingEnv, TypeElement irNodeInterface, String recordName) { + assert !recordName.contains(".") : "Record name should be simple, not qualified"; this.processingEnv = processingEnv; - var elemsInInterface = irNodeInterface.getEnclosedElements(); + this.recordName = recordName; + this.fields = getAllFields(irNodeInterface); + } + + /** Returns string representation of all necessary imports. */ + String imports() { + var importsForFields = + fields.stream() + .map(field -> "import " + field.getQualifiedTypeName() + ";") + .distinct() + .collect(Collectors.joining(System.lineSeparator())); + var allImports = IMPORTS + System.lineSeparator() + importsForFields; + return allImports; + } + + /** + * 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 getAllFields(TypeElement irNodeInterface) { + var fields = new ArrayList(); + + var elemVisitor = + new SimpleElementVisitor14() { + @Override + protected Void defaultAction(Element e, Void unused) { + for (var childElem : e.getEnclosedElements()) { + childElem.accept(this, unused); + } + return null; + } + + @Override + public Void visitExecutable(ExecutableElement e, Void unused) { + if (e.getParameters().isEmpty()) { + var retType = e.getReturnType(); + var retTypeElem = (TypeElement) processingEnv.getTypeUtils().asElement(retType); + var name = e.getSimpleName().toString(); + var childAnnot = e.getAnnotation(IRChild.class); + boolean isChild = false; + boolean isNullable = false; + if (childAnnot != null) { + ensureIsSubtypeOfIR(retTypeElem); + isChild = true; + isNullable = !childAnnot.required(); + } + fields.add(new Field(retTypeElem, name, isNullable, isChild)); + } + return super.visitExecutable(e, unused); + } + }; + irNodeInterface.accept(elemVisitor, null); + return fields; + } + + private void ensureIsSubtypeOfIR(TypeElement typeElem) { + if (!Utils.isSubtypeOfIR(typeElem.asType(), processingEnv)) { + Utils.printError( + "Method annotated with @IRChild must return a subtype of IR interface", + typeElem, + processingEnv.getMessager()); + } + } + + /** + * Returns string representation of record fields. Meant to be inside the generated record + * definition. + */ + String fields() { + var userDefinedFields = + fields.stream() + .map(field -> field.getSimpleTypeName() + " " + field.getName()) + .collect(Collectors.joining(", " + System.lineSeparator())); + var code = + """ + $userDefinedFields, + DiagnosticStorage diagnostics, + MetadataStorage passData, + IdentifiedLocation location + """ + .replace("$userDefinedFields", userDefinedFields); + return indent(code, 2); } /** @@ -95,7 +197,86 @@ public String showCode(int indent) { throw new UnsupportedOperationException("unimplemented"); } """; - var indentedCode = code.lines().map(line -> " " + line).collect(Collectors.joining("\n")); - return indentedCode; + return indent(code, 2); + } + + /** + * Returns string representation of the code for the builder - that is a nested class that allows + * to build the record. + * + * @return Code of the builder + */ + String builder() { + var fieldDeclarations = + fields.stream() + .map( + field -> + """ + $fieldType $fieldName; + """ + .replace("$fieldName", field.getName()) + .replace("$fieldType", field.getSimpleTypeName())) + .collect(Collectors.joining(System.lineSeparator())); + + var fieldSetters = + fields.stream() + .map( + field -> + """ + public Builder $fieldName($fieldType $fieldName) { + this.$fieldName = $fieldName; + return this; + } + """ + .replace("$fieldName", field.getName()) + .replace("$fieldType", field.getSimpleTypeName())) + .collect(Collectors.joining(System.lineSeparator())); + + // Validation code for all non-nullable fields + var validationCode = + fields.stream() + .filter(field -> !field.isNullable()) + .map( + field -> + """ + if (this.$fieldName == null) { + throw new IllegalArgumentException("$fieldName is required"); + } + """ + .replace("$fieldName", field.getName())) + .collect(Collectors.joining(System.lineSeparator())); + + var fieldList = fields.stream().map(Field::getName).collect(Collectors.joining(", ")); + + var code = + """ + public static final class Builder { + $fieldDeclarations + + $fieldSetters + + public $recordName build() { + validate(); + // DiagnosticStorage, MetadataStorage, IdentifiedLocation are null initially. + return new $recordName($fieldList, null, null, null); + } + + private void validate() { + $validationCode + } + } + """ + .replace("$fieldDeclarations", fieldDeclarations) + .replace("$fieldSetters", fieldSetters) + .replace("$recordName", recordName) + .replace("$fieldList", fieldList) + .replace("$validationCode", validationCode); + return indent(code, 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 index e963f1a1a64e..608818ec2d1b 100644 --- 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 @@ -12,7 +12,6 @@ import javax.lang.model.element.ElementKind; import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; -import javax.tools.Diagnostic.Kind; import javax.tools.JavaFileObject; import org.enso.runtime.parser.dsl.IRChild; import org.enso.runtime.parser.dsl.IRNode; @@ -62,7 +61,7 @@ private void processIrNode(Element irNodeElem) { printError("Failed to create source file for IRNode", irNodeElem); } assert srcGen != null; - var irNodeElement = new IRNodeElement(processingEnv, irNodeTypeElem); + var irNodeElement = new IRNodeElement(processingEnv, irNodeTypeElem, newRecordName); try { try (var lineWriter = new PrintWriter(srcGen.openWriter())) { var code = @@ -70,15 +69,24 @@ private void processIrNode(Element irNodeElem) { $imports public record $recordName ( - String field + $fields ) implements $interfaceName { + + public static Builder builder() { + return new Builder(); + } + $overrideIRMethods + + $builder } """ - .replace("$imports", IRNodeElement.IMPORTS) + .replace("$imports", irNodeElement.imports()) + .replace("$fields", irNodeElement.fields()) .replace("$recordName", newRecordName) .replace("$interfaceName", irNodeInterfaceName) - .replace("$overrideIRMethods", irNodeElement.overrideIRMethods()); + .replace("$overrideIRMethods", irNodeElement.overrideIRMethods()) + .replace("$builder", irNodeElement.builder()); lineWriter.println(code); lineWriter.println(); } @@ -96,9 +104,7 @@ private String packageName(Element elem) { } private boolean isSubtypeOfIR(TypeMirror type) { - var irType = - processingEnv.getElementUtils().getTypeElement("org.enso.compiler.core.IR").asType(); - return processingEnv.getTypeUtils().isAssignable(type, irType); + return Utils.isSubtypeOfIR(type, processingEnv); } private Set findChildElements(Element irNodeElem) { @@ -108,6 +114,6 @@ private Set findChildElements(Element irNodeElem) { } private void printError(String msg, Element elem) { - processingEnv.getMessager().printMessage(Kind.ERROR, msg, elem); + Utils.printError(msg, elem, processingEnv.getMessager()); } } diff --git a/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/Utils.java b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/Utils.java new file mode 100644 index 000000000000..0bad76e1614f --- /dev/null +++ b/engine/runtime-parser-processor/src/main/java/org/enso/runtime/parser/processor/Utils.java @@ -0,0 +1,21 @@ +package org.enso.runtime.parser.processor; + +import javax.annotation.processing.Messager; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.type.TypeMirror; +import javax.tools.Diagnostic.Kind; + +final class Utils { + private Utils() {} + + static boolean isSubtypeOfIR(TypeMirror type, ProcessingEnvironment processingEnv) { + var irType = + processingEnv.getElementUtils().getTypeElement("org.enso.compiler.core.IR").asType(); + return processingEnv.getTypeUtils().isAssignable(type, irType); + } + + static void printError(String msg, Element elem, Messager messager) { + messager.printMessage(Kind.ERROR, msg, elem); + } +} diff --git a/engine/runtime-parser-processor/src/test/java/org/enso/runtime/parser/processor/test/TestIRProcessor.java b/engine/runtime-parser-processor/src/test/java/org/enso/runtime/parser/processor/test/TestIRProcessor.java index 0dc7f11395eb..9fe0ae548e0e 100644 --- a/engine/runtime-parser-processor/src/test/java/org/enso/runtime/parser/processor/test/TestIRProcessor.java +++ b/engine/runtime-parser-processor/src/test/java/org/enso/runtime/parser/processor/test/TestIRProcessor.java @@ -6,6 +6,7 @@ import com.google.testing.compile.CompilationSubject; import com.google.testing.compile.Compiler; import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; import org.enso.runtime.parser.processor.IRProcessor; import org.junit.Test; @@ -71,6 +72,43 @@ public interface JName extends IR {} var compilation = compiler.compile(src); CompilationSubject.assertThat(compilation).succeeded(); CompilationSubject.assertThat(compilation).generatedSourceFile("JNameGen").isNotNull(); + var genSrc = compilation.generatedSourceFile("JNameGen"); + assertThat(genSrc.isPresent(), is(true)); + var srcContent = readSrcFile(genSrc.get()); assertThat("Generated just one source", compilation.generatedSourceFiles().size(), is(1)); } + + @Test + public void simpleIRNodeWithChild() { + var src = + JavaFileObjects.forSourceString( + "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.JExpression; + + @IRNode + public interface MyIR extends IR { + @IRChild JExpression expression(); + } + """); + var compiler = Compiler.javac().withProcessors(new IRProcessor()); + var compilation = compiler.compile(src); + CompilationSubject.assertThat(compilation).succeeded(); + CompilationSubject.assertThat(compilation).generatedSourceFile("MyIRGen").isNotNull(); + var genSrc = compilation.generatedSourceFile("MyIRGen"); + assertThat(genSrc.isPresent(), is(true)); + var srcContent = readSrcFile(genSrc.get()); + assertThat("Generated just one source", compilation.generatedSourceFiles().size(), is(1)); + } + + private static String readSrcFile(JavaFileObject src) { + try { + return src.getCharContent(true).toString(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } }