Skip to content

Commit

Permalink
Migrate some passes to Mini passes (#11191)
Browse files Browse the repository at this point in the history
Gets ready for avoiding IR traversal by introducing _mini passes_ as proposed by #10981:
- creates [MiniPassFactory](762045a) (that extends common `IRProcessingPass`) to transform an `IR` element to another `IR` element
- modifies `PassManager` to recognize such _mini passes_ and treat them in a special way - by using `MiniIRPass.compile`
- `MiniIRPass.compile` is using `IR.mapExpressions` to traverse the `IR` - alternative approach [withNewChildren](1abc70d) rejected for now, see _future work_ for details
- unlike _mega passes_ `IRMiniPass.compile` **does not  recursively** traverse, but with 0964711 it invokes each _mini pass_ at constant stack depth - way better for profiling
- `MiniIRPass.prepare` _works on edges_ since ffd27df - there is `IRMiniPass prepare(parent, child)` to collect information while pre-order traversing from a particular `IR` parent to a particular `IR` child
- `PassManager` rewritten to group _subsequent mini passes_ together by `MiniIRPass.combine` and really and traverse the `IR` just once - done in 2736a76
- converted to _mini pass_: `LambdaShorthandToLambda`, `OperatorToFunction`, `SectionsToBinOp` and `TailCall`
- tested for 1:1 compatibility by [converting original code to test code](f54ba6d) and _comparing `IR` produced by old and new_ implementations
  • Loading branch information
Akirathan authored Oct 15, 2024
1 parent 07d0015 commit b36fd1c
Show file tree
Hide file tree
Showing 48 changed files with 3,088 additions and 1,106 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,28 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
import org.enso.compiler.context.InlineContext;
import org.enso.compiler.context.ModuleContext;
import org.enso.compiler.core.IR;
import org.enso.compiler.core.ir.Expression;
import org.enso.compiler.core.ir.Module;
import org.enso.compiler.pass.IRPass;
import org.enso.compiler.pass.IRProcessingPass;
import scala.collection.immutable.Seq;

/** A pass that just dumps IR to the local {@code ir-dumps} directory. See {@link IRDumper}. */
public class IRDumperPass implements IRPass {
public static final IRDumperPass INSTANCE = new IRDumperPass();
private UUID uuid;

private IRDumperPass() {}

@Override
public UUID key() {
return uuid;
}

@Override
public void org$enso$compiler$pass$IRPass$_setter_$key_$eq(UUID v) {
this.uuid = v;
}

@Override
public Seq<IRPass> precursorPasses() {
public Seq<IRProcessingPass> precursorPasses() {
return nil();
}

@Override
public Seq<IRPass> invalidatedPasses() {
public Seq<IRProcessingPass> invalidatedPasses() {
return nil();
}

Expand Down Expand Up @@ -68,8 +57,8 @@ public <T extends IR> T updateMetadataInDuplicate(T sourceIr, T copyOfIr) {
}

@SuppressWarnings("unchecked")
private static scala.collection.immutable.List<IRPass> nil() {
private static scala.collection.immutable.List<IRProcessingPass> nil() {
Object obj = scala.collection.immutable.Nil$.MODULE$;
return (scala.collection.immutable.List<IRPass>) obj;
return (scala.collection.immutable.List<IRProcessingPass>) obj;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.enso.compiler.pass;

import java.util.Objects;
import org.enso.compiler.core.IR;
import org.enso.compiler.core.ir.Expression;

/** Utility class for chaining mini passes together. */
final class ChainedMiniPass extends MiniIRPass {
private final MiniIRPass firstPass;
private final MiniIRPass secondPass;

private ChainedMiniPass(MiniIRPass firstPass, MiniIRPass secondPass) {
this.firstPass = firstPass;
this.secondPass = secondPass;
}

static MiniIRPass chain(MiniIRPass firstPass, MiniIRPass secondPass) {
if (firstPass == null) {
return secondPass;
}
return new ChainedMiniPass(firstPass, secondPass);
}

@Override
public MiniIRPass prepare(IR parent, Expression current) {
var first = firstPass.prepare(parent, current);
var second = secondPass.prepare(parent, current);
if (first == firstPass && second == secondPass) {
return this;
} else {
return new ChainedMiniPass(first, second);
}
}

@Override
public Expression transformExpression(Expression ir) {
var fstIr = firstPass.transformExpression(ir);
var sndIr = secondPass.transformExpression(fstIr);
return sndIr;
}

@Override
public boolean checkPostCondition(IR ir) {
return firstPass.checkPostCondition(ir) && secondPass.checkPostCondition(ir);
}

@Override
public String toString() {
return Objects.toString(firstPass) + ":" + Objects.toString(secondPass);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.enso.compiler.pass;

import org.enso.compiler.core.ir.ProcessingPass;
import scala.collection.immutable.Seq;

/**
* A generic {@link IR} processing pass. Currently with two subclasses: classical {@link IRPass mega
* IR processing pass} and {@link MiniPassFactory}.
*/
public interface IRProcessingPass extends ProcessingPass {
/** The passes that this pass depends _directly_ on to run. */
public Seq<? extends IRProcessingPass> precursorPasses();

/** The passes that are invalidated by running this pass. */
public Seq<? extends IRProcessingPass> invalidatedPasses();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package org.enso.compiler.pass;

import java.util.function.Function;
import org.enso.compiler.core.IR;
import org.enso.compiler.core.ir.Expression;
import org.enso.compiler.core.ir.Module;

/**
* Mini IR pass operates on a single IR element at a time. The {@link org.enso.compiler.Compiler}
* traverses the whole IR tree in DFS. It works in two phases.
*
* <p><b>Note</b> that the current implementation is limited to traverse only {@link
* org.enso.compiler.core.ir.Expression} elements as provided by {@link
* IR#mapExpressions(Function)}. Hence, the additional method {@link #transformModule(Module)}.
*
* <p>In the first, <b>prepare</b> phase, the compiler traverses from the root to the leaves and
* calls the {@link #prepare(Expression)} method on the mini pass. During this phase, the mini pass
* can gather information about the current IR element, but not modify it.
*
* <p>In the second, <b>transform</b> phase, the compiler returns from the leaves to the root and
* calls the {@link #transformExpression(Expression)} method on the mini pass. During this phase,
* the mini pass is free to transform the current IR element. The children of the current IR element
* are already transformed.
*
* <p>For each IR element:
*
* <ol>
* <li>The {@link #prepare(Expression)} method is called to prepare the pass for the current IR
* element. This method is called when the {@link org.enso.compiler.Compiler} traverses the IR
* tree from top to bottom. This is useful for mini passes that need to build some information
* about the current IR element before transforming it. The mini pass must not modify the IR
* element neither attach any metadata to it in this method. By returning {@code null} from
* this method, the mini pass signals to the compiler that it wishes to not process the
* subtree of the current IR element.
* <li>The {@link #transformExpression(Expression)} method is called to transform the current IR
* element. This method is called when the {@link org.enso.compiler.Compiler} traverses the
* element from bottom to top. All the children of the current IR element are already
* transformed when this method is called.
* </ol>
*
* <p>Inspired by: <a href="https://dl.acm.org/doi/10.1145/3140587.3062346">Miniphases: compilation
* using modular and efficient tree transformations</a>. PDF available at <a
* href="https://infoscience.epfl.ch/server/api/core/bitstreams/8ab72c0a-8aa6-4dee-a704-3504938dc316/content">infoscience.epfl.ch</a>
*/
public abstract class MiniIRPass {
/**
* Prepare the pass for the provided IR element. This method is called when the {@link
* org.enso.compiler.Compiler} traverses the IR element from top to bottom.
*
* <p>The mini pass is free to gather any information about the elements it encounters (via this
* method) and use it in the {@link #transformExpression(Expression)} method. Note however, that
* it is not wise to store the references to the IR or their children for later comparison in the
* {@link #transformExpression(Expression) transform phase}, as the IR tree will most likely be
* transformed during the compilation process.
*
* <p>TL;DR; Do no store references to the IR elements or their children in this method.
*
* @param parent the the parent of the edge
* @param child the child expression element to be be processed.
* @return an instance of the pass to process the child's element subtree
*/
public MiniIRPass prepare(IR parent, Expression child) {
return this;
}

/**
* Transform the provided IR element. Children of the IR element are already transformed when this
* method is called. This method is called when the {@link org.enso.compiler.Compiler} traverses
* the IR element from bottom to top.
*
* <p>The pass should not do any traversal in this method.
*
* @param expr Expression IR element to be transformed by this pass.
* @return The transformed Expression IR, or the same IR if no transformation is needed. Must not
* return null.
*/
public abstract Expression transformExpression(Expression expr);

/**
* Transforms the module IR. This is the last method that is called.
*
* @see #transformExpression(Expression)
*/
public Module transformModule(Module moduleIr) {
return moduleIr;
}

public boolean checkPostCondition(IR ir) {
return true;
}

/**
* Name of the mini pass.
*
* @return by default it returns name of the implementing class
*/
@Override
public String toString() {
return getClass().getName();
}

/**
* Combines two mini IR passes into one that delegates to both of them.
*
* @param first first mini pass (can be {@code null})
* @param second second mini pass
* @return a combined pass that calls both non-{@code null} of the provided passes
*/
public static MiniIRPass combine(MiniIRPass first, MiniIRPass second) {
return ChainedMiniPass.chain(first, second);
}

/**
* Takes an IR element of given type {@code irType} and transforms it by provided {@link
* MiniIRPass}. When assertions are on, the resulting IR is checked with {@link
* #checkPostCondition} method of provided {@code miniPass}.
*
* @param <T> the in and out type of IR
* @param irType class of the requested IR type
* @param ir the IR element (not {@code null})
* @param miniPass the pass to apply
* @return the transformed IR
*/
public static <T extends IR> T compile(Class<T> irType, T ir, MiniIRPass miniPass) {
var newIr = MiniPassTraverser.compileDeep(ir, miniPass);
assert irType.isInstance(newIr)
: "Expected "
+ irType.getName()
+ " but got "
+ newIr.getClass().getName()
+ " by "
+ miniPass;
assert miniPass.checkPostCondition(newIr) : "Post condition failed for " + miniPass;
return irType.cast(newIr);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.enso.compiler.pass;

import org.enso.compiler.context.InlineContext;
import org.enso.compiler.context.ModuleContext;

/**
* Mini IR pass operates on a single IR element at a time. The {@link org.enso.compiler.Compiler}
* traverses the whole IR tree in DFS. The actual work is done by {@link MiniIRPass} implementation.
* This factory only contains a collection of factory methods to create such {@link MiniIRPass IR
* mini passes}. If a mini pass supports only inline compilation, its {@link
* #createForModuleCompilation(ModuleContext)} method should return null.
*/
public interface MiniPassFactory extends IRProcessingPass {
/**
* Creates an instance of mini pass that is capable of transforming IR elements in the context of
* a module.
*
* @param moduleContext A mini pass can optionally save reference to this module context.
* @return May return {@code null} if module compilation is not supported.
*/
MiniIRPass createForModuleCompilation(ModuleContext moduleContext);

/**
* Creates an instance of mini pass that is capable of transforming IR elements in the context of
* an inline compilation.
*
* @param inlineContext A mini pass can optionally save reference to this inline context.
* @return Must not return {@code null}. Inline compilation should always be supported.
*/
MiniIRPass createForInlineCompilation(InlineContext inlineContext);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package org.enso.compiler.pass;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import org.enso.compiler.core.IR;
import org.enso.compiler.core.ir.Expression;
import org.enso.compiler.core.ir.Module;

/** Implementation of {@link MiniIRPass#compile}. */
final class MiniPassTraverser {
private final MiniIRPass miniPass;
private List<IR> in;
private final List<IR> out;
private int outIndex;

private MiniPassTraverser(MiniIRPass miniPass, List<IR> out, int outIndex) {
this.miniPass = miniPass;
this.out = out;
this.outIndex = outIndex;
}

private boolean enqueue(Collection<MiniPassTraverser> queue) {
if (in == null) {
var ir = out.get(outIndex);
in = enqueueSubExpressions(queue, ir, miniPass);
return !in.isEmpty();
} else {
return false;
}
}

private void convertExpression() {
if (outIndex != -1) {
var oldIr = out.get(outIndex);
var index = new int[1];
var newIr = oldIr.mapExpressions((old) -> (Expression) in.get(index[0]++));
var transformedIr =
switch (newIr) {
case Module m -> miniPass.transformModule(m);
case Expression e -> miniPass.transformExpression(e);
default -> throw new IllegalArgumentException("" + oldIr);
};
if (oldIr != transformedIr) {
out.set(outIndex, transformedIr);
}
outIndex = -1;
}
}

static IR compileDeep(IR root, MiniIRPass miniPass) {
var result = new IR[] {root};
var rootTask = new MiniPassTraverser(miniPass, Arrays.asList(result), 0);
var stackOfPendingIrs = new LinkedList<MiniPassTraverser>();
stackOfPendingIrs.add(rootTask);
while (!stackOfPendingIrs.isEmpty()) {
if (stackOfPendingIrs.peekLast().enqueue(stackOfPendingIrs)) {
// continue descent
continue;
}
var deepestIr = stackOfPendingIrs.removeLast();
deepestIr.convertExpression();
}
assert result[0] != null;
return result[0];
}

/**
* @param queue queue to put objects in
* @param ir IR to process
* @param miniPass process with this mini pass
* @return {@code true} if the has been modified with new tries to process first
*/
private static List<IR> enqueueSubExpressions(
Collection<MiniPassTraverser> queue, IR ir, MiniIRPass miniPass) {
var childExpressions = new ArrayList<IR>();
var i = new int[1];
ir.mapExpressions(
(ch) -> {
var preparedMiniPass = miniPass.prepare(ir, ch);
childExpressions.add(ch);
queue.add(new MiniPassTraverser(preparedMiniPass, childExpressions, i[0]++));
return ch;
});
return childExpressions;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
@Persistable(clazz = GlobalNames$.class, id = 1205)
@Persistable(clazz = IgnoredBindings$.class, id = 1206)
@Persistable(clazz = Patterns$.class, id = 1207)
@Persistable(clazz = TailCall$.class, id = 1208)
@Persistable(clazz = TailCall.class, id = 1208)
@Persistable(clazz = TypeNames$.class, id = 1209)
@Persistable(clazz = TypeSignatures$.class, id = 1210)
@Persistable(clazz = DocumentationComments$.class, id = 1211)
Expand All @@ -66,7 +66,7 @@
@Persistable(clazz = Graph.Link.class, id = 1266, allowInlining = false)
@Persistable(clazz = TypeInference.class, id = 1280)
@Persistable(clazz = FramePointerAnalysis$.class, id = 1281)
@Persistable(clazz = TailCall$TailPosition$Tail$.class, id = 1282)
@Persistable(clazz = TailCall.TailPosition.class, id = 1282)
public final class PassPersistance {
private PassPersistance() {}

Expand Down
Loading

0 comments on commit b36fd1c

Please sign in to comment.