diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/ircompare/IRComparator.java b/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/ircompare/IRComparator.java new file mode 100644 index 000000000000..18fe6e6bec17 --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/ircompare/IRComparator.java @@ -0,0 +1,173 @@ +package org.enso.compiler.test.ircompare; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.enso.compiler.core.IR; +import org.enso.compiler.core.ir.Empty; +import org.enso.compiler.core.ir.Expression; +import org.enso.compiler.core.ir.Name; +import org.enso.test.utils.IRDumperTestWrapper; +import scala.jdk.javaapi.CollectionConverters; + +public final class IRComparator { + private final String name; + private final boolean compareMeta; + private final IRDumperTestWrapper dumper = new IRDumperTestWrapper(); + private IR expectedRoot; + private IR actualRoot; + + private IRComparator(String name, boolean compareMeta) { + this.name = name; + this.compareMeta = compareMeta; + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Compares IRs and dumps the diff the {@code actualIR} IR does not match the {@code expectedIR} + * one. IRs are compared recursively. {@code expectedIR} IR can have {@link org.enso.compiler.core.ir.Empty} nodes. When + * the {@link IRComparator} encounters {@link org.enso.compiler.core.ir.Empty} node in the {@code expectedIR} IR, the + * corresponding subtree in the {@code actualIR} IR is skipped. + * + *

Traverses the IRs in the BFS order. + * + *

If the comparison fails, the diff is dumped to the {@link IRDumperTestWrapper IGV}. And + * {@link IRComparisonFailure} is thrown. + * + * @param expectedIR Can have {@link SkipIR} nodes. + * @param actualIR + */ + public void compare(IR expectedIR, IR actualIR) throws IRComparisonFailure { + expectedRoot = expectedIR; + actualRoot = actualIR; + var nodesToProcess = new ArrayDeque(); + nodesToProcess.add(new NodePair(expectedIR, actualIR)); + while (!nodesToProcess.isEmpty()) { + var nodePairToProcess = nodesToProcess.poll(); + var expected = nodePairToProcess.expected; + var actual = nodePairToProcess.actual; + if (expected instanceof Empty) { + continue; + } + compareTwoNodes(expected, actual); + var children = zipChildren(expected, actual); + nodesToProcess.addAll(children); + } + } + + private void compareTwoNodes(IR expectedNode, IR actualNode) throws IRComparisonFailure { + assert !(expectedNode instanceof SkipIR); + var expectedClass = expectedNode.getClass().getName(); + var actualClass = actualNode.getClass().getName(); + if (!expectedClass.equals(actualClass)) { + throw fail( + "Expected node class " + expectedClass + " but got " + actualClass, + expectedNode, + actualNode); + } + var expectedChildrenCnt = expectedNode.children().size(); + var actualChildrenCnt = actualNode.children().size(); + if (expectedChildrenCnt != actualChildrenCnt) { + throw fail( + "Expected node to have " + expectedChildrenCnt + " children but got " + actualChildrenCnt, + expectedNode, + actualNode); + } + switch (expectedNode) { + case Name lit -> compareTwoNodes(lit, (Name) actualNode); + default -> {} + } + } + + private void compareTwoNodes(Name expectedName, Name actualName) { + if (!expectedName.name().equals(actualName.name())) { + throw fail( + "Expected Name " + expectedName.name() + " but got " + actualName.name(), + expectedName, + actualName); + } + if (expectedName.isMethod() != actualName.isMethod()) { + throw fail( + "isMethod is different: expected: " + + expectedName.isMethod() + + " vs actual: " + + actualName.isMethod(), + expectedName, + actualName); + } + } + + private static List zipChildren(IR expected, IR actual) { + assert expected.children().size() == actual.children().size(); + var children = new ArrayList(); + for (var i = 0; i < expected.children().size(); i++) { + var expectedChild = expected.children().apply(i); + var actualChild = actual.children().apply(i); + children.add(new NodePair(expectedChild, actualChild)); + } + return children; + } + + private IRComparisonFailure fail(String msg, IR expected, IR actual) { + dumper.dump(expectedRoot, name, "expected"); + dumper.dump(actualRoot, name, "actual"); + System.err.println("Dumped expected and actual IRs to IGV with name " + name); + return new IRComparisonFailure(msg, expected, actual); + } + + private IR copyRootWithSwap(IR root, Expression node, Expression replacement) { + var duplRoot = root.duplicate(false, false, false, false); + var nodesToProcess = new ArrayDeque(); + nodesToProcess.add(duplRoot); + while (!nodesToProcess.isEmpty()) { + var nodeToProcess = nodesToProcess.poll(); + if (nodeToProcess == node) { + + } + } + } + + private IR replaceChild(IR node, int childIdx, IR replacement) { + var children = node.children(); + var newChildren = new ArrayList(); + for (var i = 0; i < children.size(); i++) { + if (childIdx == i) { + newChildren.add(replacement); + } else { + newChildren.add(children.apply(i)); + } + } + // Replace via reflection + var newChildrenList = CollectionConverters.asScala(newChildren).toList(); + throw new UnsupportedOperationException("unimplemented"); + } + + private record NodePair(IR expected, IR actual) {} + + public static final class Builder { + private String name; + private boolean compareMeta = false; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder compareMeta(boolean value) { + this.compareMeta = value; + return this; + } + + public IRComparator build() { + Objects.requireNonNull(name); + if (compareMeta) { + throw new IllegalArgumentException("Meta comparison is not supported yet"); + } + return new IRComparator(name, compareMeta); + } + } +} diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/ircompare/IRComparisonFailure.java b/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/ircompare/IRComparisonFailure.java new file mode 100644 index 000000000000..9fe4acff6127 --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/ircompare/IRComparisonFailure.java @@ -0,0 +1,22 @@ +package org.enso.compiler.test.ircompare; + +import org.enso.compiler.core.IR; + +public final class IRComparisonFailure extends AssertionError { + private final IR expected; + private final IR actual; + + public IRComparisonFailure(String message, IR expected, IR actual) { + super(message); + this.expected = expected; + this.actual = actual; + } + + public IR getExpected() { + return expected; + } + + public IR getActual() { + return actual; + } +} diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/ircompare/SkipIR.java b/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/ircompare/SkipIR.java new file mode 100644 index 000000000000..1e06238d90da --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/ircompare/SkipIR.java @@ -0,0 +1,75 @@ +package org.enso.compiler.test.ircompare; + +import java.util.UUID; +import java.util.function.Function; +import org.enso.compiler.core.IR; +import org.enso.compiler.core.Identifier; +import org.enso.compiler.core.ir.DiagnosticStorage; +import org.enso.compiler.core.ir.Expression; +import org.enso.compiler.core.ir.IdentifiedLocation; +import org.enso.compiler.core.ir.MetadataStorage; +import scala.Option; +import scala.collection.immutable.List; + +/** + * An IR node, whose subtree is skipped by {@link IRComparator}. + */ +public final class SkipIR implements Expression { + public static final SkipIR INSTANCE = new SkipIR(); + + private SkipIR() {} + + @Override + public MetadataStorage passData() { + throw new UnsupportedOperationException("unimplemented"); + } + + @Override + public IdentifiedLocation identifiedLocation() { + throw new UnsupportedOperationException("unimplemented"); + } + + @Override + public Expression setLocation(Option location) { + throw new UnsupportedOperationException("unimplemented"); + } + + @Override + public Expression mapExpressions(Function fn) { + throw new UnsupportedOperationException("unimplemented"); + } + + @Override + public List children() { + throw new UnsupportedOperationException("unimplemented"); + } + + @Override + public @Identifier UUID getId() { + throw new UnsupportedOperationException("unimplemented"); + } + + @Override + public DiagnosticStorage diagnostics() { + throw new UnsupportedOperationException("unimplemented"); + } + + @Override + public DiagnosticStorage getDiagnostics() { + throw new UnsupportedOperationException("unimplemented"); + } + + @Override + public Expression duplicate( + boolean keepLocations, + boolean keepMetadata, + boolean keepDiagnostics, + boolean keepIdentifiers) { + throw new UnsupportedOperationException("unimplemented"); + } + + @Override + public String showCode(int indent) { + throw new UnsupportedOperationException("unimplemented"); + } +} diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/ircompare/TestComparator.java b/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/ircompare/TestComparator.java new file mode 100644 index 000000000000..5c05b55f1023 --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/ircompare/TestComparator.java @@ -0,0 +1,49 @@ +package org.enso.compiler.test.ircompare; + +import static scala.jdk.javaapi.CollectionConverters.asScala; + +import java.util.List; +import org.enso.compiler.core.ir.Expression; +import org.enso.compiler.core.ir.MetadataStorage; +import org.enso.compiler.core.ir.Name; +import org.junit.Test; +import scala.Option; + +public final class TestComparator { + @Test + public void compareSingleNodes() { + var expectedLit = lit("foo", false); + var actualLit = lit("foo", false); + var comparator = buildComparator("compareSingleNodes"); + comparator.compare(expectedLit, actualLit); + } + + @Test + public void compareBlocks() { + var expectedBlock = block(List.of(), lit("foo", false)); + var actualBlock = block(List.of(), lit("foo", false)); + var comparator = buildComparator("compareBLocks"); + comparator.compare(expectedBlock, actualBlock); + } + + @Test + public void compareBlocksWithSkip() { + var expected = block(List.of(SkipIR.INSTANCE), lit("foo", false)); + var actual = block(List.of(lit("XXX", false)), lit("foo", false)); + var comparator = buildComparator("compareBlocksWithSkip"); + comparator.compare(expected, actual); + } + + private static Name.Literal lit(String name, boolean isMethod) { + return new Name.Literal(name, isMethod, null, Option.empty(), new MetadataStorage()); + } + + private static Expression.Block block(List expressions, Expression returnExpr) { + return new Expression.Block( + asScala(expressions).toList(), returnExpr, null, false, new MetadataStorage()); + } + + private static IRComparator buildComparator(String name) { + return IRComparator.builder().name(name).build(); + } +} diff --git a/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/pass/desugar/NestedPatternMatchTest.scala b/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/pass/desugar/NestedPatternMatchTest.scala index 8afc10b451eb..36670afabf03 100644 --- a/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/pass/desugar/NestedPatternMatchTest.scala +++ b/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/pass/desugar/NestedPatternMatchTest.scala @@ -3,10 +3,11 @@ package org.enso.compiler.test.pass.desugar import org.enso.compiler.Passes import org.enso.compiler.context.{FreshNameSupply, InlineContext, ModuleContext} import org.enso.compiler.core.ir.expression.Case -import org.enso.compiler.core.ir.{Expression, Literal, Module, Name, Pattern} +import org.enso.compiler.core.ir.{Empty, Expression, Literal, Module, Name, Pattern} import org.enso.compiler.pass.desugar.NestedPatternMatch import org.enso.compiler.pass.{PassConfiguration, PassGroup, PassManager} -import org.enso.compiler.test.CompilerTest +import org.enso.compiler.test.ircompare.{IRComparator} +import org.enso.compiler.test.{CompilerTest} class NestedPatternMatchTest extends CompilerTest { @@ -342,4 +343,87 @@ class NestedPatternMatchTest extends CompilerTest { consANilBranch2Expr.branches.head.terminalBranch shouldBe true } } + + "Simple nested pattern desugaring" should { + implicit val ctx: InlineContext = mkInlineContext + + // IGV graph: https://github.com/user-attachments/assets/b5387e61-e577-4b03-8a4a-ca05e27f2462 + "One nested pattern" in { + val ir = + """ + |case x of + | Cons (Nested a) -> num + |""".stripMargin.preprocessExpression.get + val processed = ir.desugar + + val expectedIR = Expression.Block( + expressions = List( + Expression.Binding( + name = lit(""), + expression = emptyIR(), + identifiedLocation = null + ) + ), + returnValue = Case.Expr( + scrutinee = lit(""), + branches = List( + Case.Branch( + pattern = Pattern.Constructor( + constructor = lit("Cons"), + fields = List( + Pattern.Name(lit(""), identifiedLocation = null) + ), + identifiedLocation = null + ), + expression = Expression.Block( + expressions = List( + Expression.Binding( + name = lit(""), + expression = emptyIR(), + identifiedLocation = null + ), + ), + returnValue = Case.Expr( + scrutinee = lit(""), + branches = List( + Case.Branch( + pattern = Pattern.Constructor( + constructor = lit("Nested"), + fields = List( + Pattern.Name(lit("a"), identifiedLocation = null) + ), + identifiedLocation = null + ), + expression = lit("num"), + terminalBranch = true, + identifiedLocation = null + ) + ), + isNested = true, + identifiedLocation = null + ), + identifiedLocation = null + ), + identifiedLocation = null, + terminalBranch = false + ), + ), + isNested = false, + identifiedLocation = null + ), + identifiedLocation = null + ) + + val comparator = IRComparator.builder().name("One nested pattern").build() + comparator.compare(expectedIR, processed) + } + } + + private def lit(name: String): Name.Literal = { + Name.Literal(name = name, isMethod = false, identifiedLocation = null) + } + + private def emptyIR(): Empty = { + new Empty(null) + } }