Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Implement IRComparator in tests #12162

Draft
wants to merge 2 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Traverses the IRs in the BFS order.
*
* <p>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<NodePair>();
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<NodePair> zipChildren(IR expected, IR actual) {
assert expected.children().size() == actual.children().size();
var children = new ArrayList<NodePair>();
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<IR>();
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<IR>();
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<IdentifiedLocation> location) {
throw new UnsupportedOperationException("unimplemented");
}

@Override
public Expression mapExpressions(Function<Expression, Expression> fn) {
throw new UnsupportedOperationException("unimplemented");
}

@Override
public List<IR> 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");
}
}
Original file line number Diff line number Diff line change
@@ -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<Expression> 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();
}
}
Loading
Loading