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)
+ }
}