diff --git a/.gitignore b/.gitignore index bb8b7ba..425c32b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,10 @@ .metadata -build +/build codacy-coverage.json -graal +/graal jacoco.exec jacoco.xml -libs +/libs mx -src_gen +/src_gen +/docs diff --git a/.settings/BD-Tests.launch b/.settings/BD-Tests.launch new file mode 100644 index 0000000..7f7929e --- /dev/null +++ b/.settings/BD-Tests.launch @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 7bea04f..27d4bbf 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,21 @@ The benefit of this optimization is to improve interpreter performance, reduce compilation time, and possibly simplify interpreter debugging since it simplifies execution drastically. +#### 4. Inlining/Splitting: Support parse-time inlining and run-time splitting + +The `inlining` diamond provides infrastructure to support inlining at parse time +and splitting at execution time. Inlining enables us to optimize more complex +structures such as loops, iteration, selection, or other elements that often +take lambdas, closures, or other kind of anonymous methods. If they are provided +lexically, and are trivially non-accessible by language constructs, it can be +very beneficial to inline them on the AST level already to optimize execution +time in the interpreter, which can also reduce compilation time. + +This infrastructure provides the basic mechanisms that a language independent. +This includes a general visitor that can adapt lexical scopes for instance also +after simple splitting, which can be necessary, for instance to ensure that +the split methods are independent and specialize independently at run time. + License and Acknowledgements ---------------------------- diff --git a/build.xml b/build.xml index e4ff4ff..52fb44a 100644 --- a/build.xml +++ b/build.xml @@ -236,5 +236,31 @@ + + + + + + + + + + + diff --git a/src/bd/basic/IdProvider.java b/src/bd/basic/IdProvider.java index 95de708..2052a9f 100644 --- a/src/bd/basic/IdProvider.java +++ b/src/bd/basic/IdProvider.java @@ -13,6 +13,9 @@ public interface IdProvider { /** * Gets a Java string and needs to return an identifier. Typically, this is some form of * symbol or interned string that can be safely compared with reference equality. + * + * @param id the string version of the id + * @return the unique identifier */ Id getId(String id); } diff --git a/src/bd/inlining/InlinableNodes.java b/src/bd/inlining/InlinableNodes.java new file mode 100644 index 0000000..4cd6bfc --- /dev/null +++ b/src/bd/inlining/InlinableNodes.java @@ -0,0 +1,158 @@ +package bd.inlining; + +import java.lang.reflect.Constructor; +import java.util.HashMap; +import java.util.List; + +import com.oracle.truffle.api.dsl.NodeFactory; +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.source.SourceSection; + +import bd.basic.IdProvider; +import bd.basic.ProgramDefinitionError; +import bd.inlining.Inliner.FactoryInliner; +import bd.settings.VmSettings; + + +/** + * Represents the entry point to access the inlining functionality controlled with + * the @{@link Inline} annotation. + * + *

A typical use case would be in a parser, which can use the + * {@link #inline(Object, List, ScopeBuilder, SourceSection)} to request inlining. + * For this purpose, {@link InlinableNodes} takes a list of node classes and factories as + * candidates for inlining. + * + * @param the type of the identifiers used for mapping to primitives, typically some form + * of interned string construct (see {@link IdProvider}) + */ +public final class InlinableNodes { + + /** The id provider is used to map strings in the {@link Inline} annotation to ids. */ + private final IdProvider ids; + + /** Inlinable nodes for selector. */ + private final HashMap inlinableNodes; + + /** + * Initialize this registry for inlinable nodes. + * + * @param ids an id provider to convert strings to identifiers + * @param inlinableNodes list of {@link Node} classes that have the @{@link Inline} + * annotation + * @param inlinableFactories list of {@link NodeFactory}s for classes that have + * the @{@link Inline} annotation + */ + public InlinableNodes(final IdProvider ids, + final List> inlinableNodes, + final List> inlinableFactories) { + this.ids = ids; + this.inlinableNodes = new HashMap<>(); + initializeNodes(inlinableNodes); + initializeFactories(inlinableFactories); + } + + private void initializeNodes(final List> inlinableNodes) { + if (inlinableNodes == null) { + return; + } + + for (Class nodeClass : inlinableNodes) { + Inline[] ann = getInlineAnnotation(nodeClass); + assert ann != null; + + Constructor[] ctors = nodeClass.getConstructors(); + assert ctors.length == 1 : "We expect nodes marked with Inline to have only one constructor," + + " or be used via node factories."; + + for (Inline inAn : ann) { + assert !"".equals(inAn.selector()); + Id selector = ids.getId(inAn.selector()); + assert !this.inlinableNodes.containsKey(selector); + + @SuppressWarnings("unchecked") + Inliner inliner = new Inliner(inAn, (Constructor) ctors[0]); + + this.inlinableNodes.put(selector, inliner); + } + } + } + + private void initializeFactories( + final List> inlinableFactories) { + if (inlinableFactories == null) { + return; + } + + for (NodeFactory fact : inlinableFactories) { + Inline[] ann = getInlineAnnotation(fact); + assert ann != null; + + for (Inline inAn : ann) { + assert !"".equals(inAn.selector()); + Id selector = ids.getId(inAn.selector()); + + assert !this.inlinableNodes.containsKey(selector); + Inliner inliner = new FactoryInliner(inAn, fact); + this.inlinableNodes.put(selector, inliner); + } + } + } + + /** + * Try to construct an inlined version for a potential node (which is not given here, but + * would be constructed as a fall-back version). + * + *

The potential node is identified with a {@code selector} and it is determined whether + * inlining is applicable by using the data from {@link Inline} and matching it with the + * {@code argNodes}. + * + * @param the node type of the return value + * @param the type of the {@link ScopeBuilder} + * + * @param selector to identify a potential inline replacement + * @param argNodes the argument/child nodes to the potential node + * @param builder used for providing context to the inlining operation + * @param source the source section of the potential node + * @return the inlined version of the potential node, or {@code null}, if inlining is not + * applicable + * @throws ProgramDefinitionError in case the inlining would result in a structural violation + * of program definition constraints. The error is language specific and not + * triggered by the inlining logic. + */ + public > N inline(final Id selector, + final List argNodes, final S builder, final SourceSection source) + throws ProgramDefinitionError { + Inliner inliner = inlinableNodes.get(selector); + if (inliner == null || (VmSettings.DYNAMIC_METRICS && inliner.isDisabled())) { + return null; + } + + if (!inliner.matches(argNodes)) { + return null; + } + + return inliner.create(argNodes, builder, source); + } + + /** + * Get the {@link Inline} annotation from a {@link NodeFactory}. + */ + private Inline[] getInlineAnnotation(final NodeFactory factory) { + Class nodeClass = factory.getNodeClass(); + return nodeClass.getAnnotationsByType(Inline.class); + } + + /** + * Get the {@link Inline} annotation from a {@link Node} class. + */ + private Inline[] getInlineAnnotation(final Class nodeClass) { + Inline[] annotations = nodeClass.getAnnotationsByType(Inline.class); + if (annotations == null || annotations.length < 1) { + throw new IllegalArgumentException("The class " + nodeClass.getName() + + " was registered with InlinableNodes, but was not marked with @Inline." + + " Please make sure it has the right annotation."); + } + return annotations; + } +} diff --git a/src/bd/inlining/Inline.java b/src/bd/inlining/Inline.java new file mode 100644 index 0000000..9cd056c --- /dev/null +++ b/src/bd/inlining/Inline.java @@ -0,0 +1,71 @@ +package bd.inlining; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/** + * Annotation that marks nodes as inlined versions for language constructs. + * More complex nodes/constructs are to be replaced by these nodes in the parser. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@Repeatable(Inline.Container.class) +public @interface Inline { + + /** + * Selector, i.e., identifier for a node to determine that inlining applies. + * + * @return a string identifying the node + */ + String selector(); + + /** + * The arguments, by index, which need to be inlinable to make this node applicable. + * + * @return indexes of the inlinable argument nodes + */ + int[] inlineableArgIdx(); + + /** + * The argument nodes might need extra temporary variables when being inlined. + * + * @return indexes of the argument nodes that need a temporary variable + */ + int[] introduceTemps() default {}; + + /** + * Additional values to be provided as arguments to the node constructor. + * + * @return array value identifies, currently either {@link True} or {@link False} + */ + Class[] additionalArgs() default {}; + + /** + * Disabled for Dynamic Metrics. + * + * @return true if inlining should not be applied, false otherwise + */ + boolean disabled() default false; + + /** + * Represents a true value for an additional argument. + * Can be used for {@link #additionalArgs()}. + */ + class True {} + + /** + * Represents a false value for an additional argument. + * Can be used for {@link #additionalArgs()}. + */ + class False {} + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.TYPE}) + @interface Container { + Inline[] value(); + } +} diff --git a/src/bd/inlining/Inliner.java b/src/bd/inlining/Inliner.java new file mode 100644 index 0000000..b09e79c --- /dev/null +++ b/src/bd/inlining/Inliner.java @@ -0,0 +1,151 @@ +package bd.inlining; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +import com.oracle.truffle.api.dsl.NodeFactory; +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.source.SourceSection; + +import bd.basic.ProgramDefinitionError; +import bd.inlining.Inline.False; +import bd.inlining.Inline.True; +import bd.inlining.nodes.Inlinable; +import bd.inlining.nodes.WithSource; + + +class Inliner { + protected final Inline inline; + + private final Constructor ctor; + + Inliner(final Inline inline, final Constructor ctor) { + this.inline = inline; + this.ctor = ctor; + } + + public boolean isDisabled() { + return inline.disabled(); + } + + public boolean matches(final List argNodes) { + int[] args = inline.inlineableArgIdx(); + assert args != null; + + boolean allInlinable = true; + for (int i : args) { + allInlinable &= argNodes.get(i) instanceof Inlinable; + } + return allInlinable; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public N create(final List argNodes, final ScopeBuilder scopeBuilder, + final SourceSection source) throws ProgramDefinitionError { + Object[] args = new Object[argNodes.size() + inline.inlineableArgIdx().length + + inline.additionalArgs().length]; + + assert args.length == ctor.getParameterCount(); + + int i = 0; + for (N arg : argNodes) { + args[i] = arg; + i += 1; + } + + for (int a : inline.inlineableArgIdx()) { + args[i] = ((Inlinable) argNodes.get(a)).inline(scopeBuilder); + i += 1; + } + + for (Class c : inline.additionalArgs()) { + if (c == True.class) { + args[i] = true; + } else { + assert c == False.class; + args[i] = false; + } + i += 1; + } + try { + N node = (N) ctor.newInstance(args); + ((WithSource) node).initialize(source); + return node; + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + /** + * Nodes that use a factory are expected to be structured so that the first argNodes are + * going to be evaluated by the DSL, which means, they are going to be appended at the end + * of the args array. + * + *

The rest is treated as normal, first the args, then the inlined args, + * then possibly to be introduced temps, and finally possible additional args. + * + * @param + * @param + * @param + * @param + * @param + * @param + */ + static class FactoryInliner extends Inliner { + private final NodeFactory factory; + + FactoryInliner(final Inline inline, final NodeFactory factory) { + super(inline, null); + this.factory = factory; + } + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public N create(final List argNodes, final ScopeBuilder scopeBuilder, + final SourceSection source) throws ProgramDefinitionError { + Object[] args = new Object[argNodes.size() + inline.inlineableArgIdx().length + + inline.introduceTemps().length + inline.additionalArgs().length]; + + assert args.length == factory.getNodeSignatures().get(0).size(); + + int restArgs = factory.getExecutionSignature().size(); + + int i = 0; + for (int j = 0; j < argNodes.size(); j += 1) { + if (j < restArgs) { + int endOffset = args.length - restArgs + j; + args[endOffset] = argNodes.get(j); + } else { + args[i] = argNodes.get(j); + i += 1; + } + } + + for (int a : inline.inlineableArgIdx()) { + args[i] = ((Inlinable) argNodes.get(a)).inline(scopeBuilder); + i += 1; + } + + for (int a : inline.introduceTemps()) { + args[i] = scopeBuilder.introduceTempForInlinedVersion( + (Inlinable) argNodes.get(a), source); + } + + for (Class c : inline.additionalArgs()) { + if (c == True.class) { + args[i] = true; + } else { + assert c == False.class; + args[i] = false; + } + i += 1; + } + + N node = (N) factory.createNode(args); + ((WithSource) node).initialize(source); + return node; + } + } +} diff --git a/src/bd/inlining/NodeState.java b/src/bd/inlining/NodeState.java new file mode 100644 index 0000000..05f9a6e --- /dev/null +++ b/src/bd/inlining/NodeState.java @@ -0,0 +1,14 @@ +package bd.inlining; + +/** + * This {@link NodeState} is used by the node factory methods of {@link Variable} to pass in + * required state. + * It is thus merely a marker interface. + * + *

Node state can typically include information about lexical bindings, for instance in form + * of unique identifiers, which might be class names or ids of mixins or similar lexical + * constructs. + */ +public interface NodeState { + +} diff --git a/src/bd/inlining/NodeVisitorUtil.java b/src/bd/inlining/NodeVisitorUtil.java new file mode 100644 index 0000000..47c65b5 --- /dev/null +++ b/src/bd/inlining/NodeVisitorUtil.java @@ -0,0 +1,22 @@ +package bd.inlining; + +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.nodes.NodeVisitor; + +import bd.basic.nodes.DummyParent; + + +final class NodeVisitorUtil { + + @SuppressWarnings("unchecked") + public static ExprT applyVisitor(final ExprT body, + final NodeVisitor visitor) { + DummyParent dummyParent = new DummyParent(body); + + body.accept(visitor); + + // need to return the child of the dummy parent, + // since it could have been replaced + return (ExprT) dummyParent.child; + } +} diff --git a/src/bd/inlining/Scope.java b/src/bd/inlining/Scope.java new file mode 100644 index 0000000..105ee81 --- /dev/null +++ b/src/bd/inlining/Scope.java @@ -0,0 +1,58 @@ +package bd.inlining; + +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.nodes.RootNode; + + +/** + * A {@link Scope} is meant to represent a lexical construct that delineate a set of + * {@link Variable} definitions that belong together, usually based on the textual/language + * properties of the implemented language. + * + *

Many languages use lexical scopes, i.e., grouping of variables based on textual elements. + * Often such groups are delimited by parentheses or other indicators for textual blocks. + * + *

Scopes are expected to form a chain from the inner to the outer scopes. + * + * @param the concrete type of the scope + * @param the type for a run-time representation of a method, block, lambda, + * typically at the implementation level, not necessarily exposed on the language + * level. It is likely a subclass of {@link RootNode}. + */ +public interface Scope, MethodT> { + + /** + * The set of variables defined by this scope. + * + *

The set excludes variables that are defined in other scopes, even if they might be + * logically part of the set from language perspective, perhaps because of nesting scopes. + * + * @param the type of the {@link Variable} implementation + * + * @return the set of variables defined in this scope + */ + > T[] getVariables(); + + /** + * The scope directly enclosing the current scope. + * + * @return the outer scope, or {@code null} if it is already the final scope + */ + This getOuterScopeOrNull(); + + /** + * Lookup the scope corresponding to the given run-time entity, which often corresponds to a + * nested method, block, lambda, etc. + * + * @param method, the method, block, lambda, etc, for which the scope is to be determined + * @return a scope, or {@code null} + */ + This getScope(MethodT method); + + /** + * A name identifying the scope, used for debugging. + * + * @return a human-readable name for debugging + */ + String getName(); +} diff --git a/src/bd/inlining/ScopeAdaptationVisitor.java b/src/bd/inlining/ScopeAdaptationVisitor.java new file mode 100644 index 0000000..261e937 --- /dev/null +++ b/src/bd/inlining/ScopeAdaptationVisitor.java @@ -0,0 +1,244 @@ +package bd.inlining; + +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.nodes.NodeUtil; +import com.oracle.truffle.api.nodes.NodeVisitor; + +import bd.inlining.nodes.ScopeReference; + + +/** + * A Truffle AST {@link NodeVisitor} that is used to fix up {@link ScopeReference} after any + * scope changes, for instance caused by inlining or splitting. + */ +public final class ScopeAdaptationVisitor implements NodeVisitor { + + protected final Scope scope; + + protected final boolean outerScopeChanged; + + /** + * This visitor refers to the scope at the contextLevel given here, and thus, needs to apply + * its transformations to elements referring to that level. + */ + public final int contextLevel; + + /** + * Use the visitor to adapt a copy of the given {@code body} to the current scope. + * + * @param the type of the returned node + * + * @param body an AST that needs to be adapted + * @param newScope is the scope the body needs to be adapted to + * @param appliesTo the context level, which needs to be changed + * @param someOuterScopeIsMerged a flag that can possibly used for optimization to decide + * whether a node needs to be adapted + * @return a copy of {@code body} adapted to the given scope + */ + public static N adapt(final N body, final Scope newScope, + final int appliesTo, final boolean someOuterScopeIsMerged) { + N inlinedBody = NodeUtil.cloneNode(body); + + return NodeVisitorUtil.applyVisitor(inlinedBody, + new ScopeAdaptationVisitor(newScope, appliesTo, someOuterScopeIsMerged)); + } + + private ScopeAdaptationVisitor(final Scope scope, final int appliesTo, + final boolean outerScopeChanged) { + if (scope == null) { + throw new IllegalArgumentException( + "InliningVisitor requires a scope, but got scope==null"); + } + this.scope = scope; + this.contextLevel = appliesTo; + this.outerScopeChanged = outerScopeChanged; + } + + /** + * @return true if some outer scope was changed, for instance merged with another one. + */ + public boolean outerScopeChanged() { + return outerScopeChanged; + } + + /** + * The result of a lookup in the scope chain to find a variable and its context level. + * + * @param the type of node used for node access, can be very unprecise + */ + public static final class ScopeElement { + + /** The variable found by the lookup. */ + public final Variable var; + + /** + * The context level at which the variable is defined, relative to the start of the lookup. + */ + public final int contextLevel; + + private ScopeElement(final Variable var, final int contextLevel) { + this.var = var; + this.contextLevel = contextLevel; + } + + @Override + public String toString() { + return "ScopeElement[" + var.toString() + ", ctx: " + contextLevel + "]"; + } + } + + @SuppressWarnings("unchecked") + private ScopeElement getSplitVar(final Variable var, + final Scope scope, final int lvl) { + for (Variable v : scope.getVariables()) { + if (v.equals(var)) { + return new ScopeElement<>((Variable) v, lvl); + } + } + + Scope outer = scope.getOuterScopeOrNull(); + if (outer == null) { + throw new IllegalStateException("Couldn't find var: " + var.toString()); + } else { + return getSplitVar(var, outer, lvl + 1); + } + } + + /** + * Get the variable adapted to the current scope. + * + * @param var in the un-adapted node + * @return the adapted version of the variable + */ + public ScopeElement getAdaptedVar(final Variable var) { + return getSplitVar(var, scope, 0); + } + + /** + * Get the adapted scope for an embedded block, lambda, method etc. + * + * @param the scope type + * @param the type of the run-time element representing the scope + * + * @param method, the run-time element for which to determine the adapted scope + * + * @return the adapted scope for the given method + */ + @SuppressWarnings("unchecked") + public , MethodT> S getScope(final MethodT method) { + return ((S) scope).getScope(method); + } + + /** + * The current scope, which had been adapted before instantiating the visitor. + * + * @param the scope type + * @param the type of the run-time element representing the scope + * + * @return the current scope + */ + @SuppressWarnings("unchecked") + public , MethodT> S getCurrentScope() { + return (S) scope; + } + + /** + * Adapt the given node. + * + * @return true, if the process should continue + */ + @Override + public boolean visit(final Node node) { + if (node instanceof ScopeReference) { + ((ScopeReference) node).replaceAfterScopeChange(this); + } + return true; + } + + /** + * Factory method to update a read node with an appropriate version for the adapted scope. + * + * @param the type of the node to be returned + * + * @param var the variable accessed by {@code node} + * @param node the read node + * @param ctxLevel the context level of the node + */ + public void updateRead(final Variable var, final N node, + final int ctxLevel) { + ScopeElement se = getAdaptedVar(var); + if (se.var != var || se.contextLevel < ctxLevel) { + node.replace(se.var.getReadNode(se.contextLevel, node.getSourceSection())); + } else { + assert ctxLevel == se.contextLevel; + } + } + + /** + * Factory method to update a write node with an appropriate version for the adapted scope. + * + * @param the type of the node to be returned + * + * @param var the variable accessed by {@code node} + * @param node the write node + * @param valExpr the expression that is evaluated to determine the value to be written to + * the variable + * @param ctxLevel the context level of the node + */ + public void updateWrite(final Variable var, final N node, + final N valExpr, final int ctxLevel) { + ScopeElement se = getAdaptedVar(var); + if (se.var != var || se.contextLevel < ctxLevel) { + node.replace(se.var.getWriteNode(se.contextLevel, valExpr, node.getSourceSection())); + } else { + assert ctxLevel == se.contextLevel; + } + } + + /** + * Factory method to update a this-read node with an appropriate version for the + * adapted scope. + * + * @param the type of the node to be returned + * + * @param var the variable accessed by {@code node} + * @param node the {@code this} node + * @param state additional state needed to initialize the adapted node + * @param ctxLevel the context level of the node + */ + public void updateThisRead(final Variable var, final N node, + final NodeState state, final int ctxLevel) { + ScopeElement se = getAdaptedVar(var); + if (se.var != var || se.contextLevel < ctxLevel) { + node.replace(se.var.getThisReadNode(se.contextLevel, state, node.getSourceSection())); + } else { + assert ctxLevel == se.contextLevel; + } + } + + /** + * Factory method to update a super-read node with an appropriate version for + * the adapted scope. + * + * @param the type of the node to be returned + * + * @param var the variable accessed by {@code node} + * @param node the {@code super} node + * @param state additional state needed to initialize the adapted node + * @param ctxLevel the context level of the node + */ + public void updateSuperRead(final Variable var, final N node, + final NodeState state, final int ctxLevel) { + ScopeElement se = getAdaptedVar(var); + if (se.var != var || se.contextLevel < ctxLevel) { + node.replace(se.var.getSuperReadNode(se.contextLevel, state, node.getSourceSection())); + } else { + assert ctxLevel == se.contextLevel; + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[" + scope.getName() + "]"; + } +} diff --git a/src/bd/inlining/ScopeBuilder.java b/src/bd/inlining/ScopeBuilder.java new file mode 100644 index 0000000..709dea9 --- /dev/null +++ b/src/bd/inlining/ScopeBuilder.java @@ -0,0 +1,36 @@ +package bd.inlining; + +import com.oracle.truffle.api.source.SourceSection; + +import bd.basic.ProgramDefinitionError; +import bd.inlining.nodes.Inlinable; + + +/** + * Builds a {@link Scope}, typically at source processing time (i.e., source compilation time, + * which in a Truffle system refers to the time when the Truffle AST is created for execution). + * + *

A candidate for a concrete scope builder could be for instance the class that creates + * methods or lambdas when parsing code. + * + * @param the concrete type of the builder + */ +public interface ScopeBuilder> { + + /** + * Introduce a temporary variable for the inlined version of a node. + * + *

Some code elements require additional temporary variables when they are inlined. + * An example is the lambda-body for a counting loop, which needs a temporary variable to + * perform the counting and communicate it to the lambda. + * + * @param node the node using a variable that needs to be represented as a new temporary + * @param source the synthetic source location for the new variable definition + * @return the introduced variable representing the temporary + * @throws ProgramDefinitionError when there is a consistency problem caused by introducing + * the variable. This is supposed to be used to indicate errors in the user + * program. + */ + Variable introduceTempForInlinedVersion(Inlinable node, SourceSection source) + throws ProgramDefinitionError; +} diff --git a/src/bd/inlining/Variable.java b/src/bd/inlining/Variable.java new file mode 100644 index 0000000..6a603f9 --- /dev/null +++ b/src/bd/inlining/Variable.java @@ -0,0 +1,92 @@ +package bd.inlining; + +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.source.SourceSection; + + +/** + * A {@link Variable} represents a variable most often in the user code, or sometimes internal + * to the language implementation. + * + *

Generally, we expect variables to be read or written, but do not require an + * implementation the writing operation, since variables might be immutable and initialized + * otherwise. + * + *

Some special variables, such as this can require extra handling and can thus + * require the use of special nodes. We provide here factory methods for this-like + * variables as well as super-like reads. + * + *

Note that variable access are typically associated with a contextLevel. The + * precise semantics is specific to your language's use of {@link Scope}s. But generally, we + * assume that scopes are defined lexically, and a context level of 0 means the local scope, + * and every increment represents one step outwards in a scope chain. + * + * @param the type of nodes expected to be returned for reading variables + */ +public interface Variable { + + /** + * Create a node to read the value of this variable. + * + * @param contextLevel references the scope in which the variable is defined, + * relative to the scope in which the read is done + * @param source of the read operation + * @return a node to read this variable + */ + N getReadNode(int contextLevel, SourceSection source); + + /** + * Create a node to write to this variable. + * + * @param contextLevel references the scope in which the variable is defined, + * relative to the scope in which the write is done + * @param valueExpr is the expression that needs to be evaluated to determine the value, + * which is to be written to the variable + * @param source of the write operation + * @return a node to write this variable + */ + default N getWriteNode(final int contextLevel, final N valueExpr, + final SourceSection source) { + throw new UnsupportedOperationException( + "Variable.getWriteNode not supported on this type of variable: " + + getClass().getSimpleName()); + } + + /** + * Create a node to read the special this variable. + * + *

This operation should only be used on variables that are this-like + * variables. + * + * @param contextLevel references the scope in which the variable is defined, + * relative to the scope in which the read is done + * @param state to be used to initialize this node + * @param source of the read operation + * @return a node to read this + */ + default N getThisReadNode(final int contextLevel, final NodeState state, + final SourceSection source) { + throw new UnsupportedOperationException( + "Variable.getThisReadNode not supported on this type of variable: " + + getClass().getSimpleName()); + } + + /** + * Create a node to read the special super variable. + * + *

This operation should only be used on variables that are this-like + * variables supporting super reads. + * + * @param contextLevel references the scope in which the variable is defined, + * relative to the scope in which the read is done + * @param state to be used to initialize this node + * @param source of the read operation + * @return a node to read super + */ + default N getSuperReadNode(final int contextLevel, final NodeState state, + final SourceSection source) { + throw new UnsupportedOperationException( + "Variable.getSuperReadNode not supported on this type of variable: " + + getClass().getSimpleName()); + } +} diff --git a/src/bd/inlining/nodes/Inlinable.java b/src/bd/inlining/nodes/Inlinable.java new file mode 100644 index 0000000..64a7814 --- /dev/null +++ b/src/bd/inlining/nodes/Inlinable.java @@ -0,0 +1,37 @@ +package bd.inlining.nodes; + +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.nodes.RootNode; + +import bd.inlining.Inline; +import bd.inlining.ScopeBuilder; + + +/** + * Inlinable nodes can be replaced, inlined with arbitrary other nodes. + * + *

Note, {@link Inlinable} is different from {@link Inline}. Nodes marked + * with @{@link Inline} are replacing more general nodes, which typically have + * Inlinable nodes as their children. + * + *

In most cases, the replacement nodes for an Inlinable node will be at least + * logically sub-nodes of the inlinable node. + * + *

An example for an Inlinable node is a lambda (closure, block) that + * represents for instance the body of a for-each operation. The for-each operation can be + * annotated with @{@link Inline}, so that it's children, which include the node representing + * the lambda are then inlined. Concretely, the node to replace the Inlinable node + * would likely be the {@link RootNode} of the lambda. + * + * @param the concrete type of the used {@link ScopeBuilder} + */ +public interface Inlinable> { + + /** + * Inline the give node in the context defined by {@code ScopeBuilder}. + * + * @param scopeBuilder defines the context for the inlining + * @return a new node that represents the inlined behavior + */ + Node inline(SB scopeBuilder); +} diff --git a/src/bd/inlining/nodes/ScopeReference.java b/src/bd/inlining/nodes/ScopeReference.java new file mode 100644 index 0000000..856a758 --- /dev/null +++ b/src/bd/inlining/nodes/ScopeReference.java @@ -0,0 +1,35 @@ +package bd.inlining.nodes; + +import com.oracle.truffle.api.frame.Frame; +import com.oracle.truffle.api.frame.FrameSlot; +import com.oracle.truffle.api.nodes.Node; + +import bd.inlining.ScopeAdaptationVisitor; + + +/** + * All {@link Node} classes that potentially reference the scope in some way should implement + * the {@link ScopeReference} interface. + * + *

A reference to the scope is typically an access to some type of variable, which might be + * realized as an access to a {@link Frame} with a {@link FrameSlot}. More generally, it is any + * reference that might need to be adjusted after scopes have been changed. Scope changes can + * be cause by inlining, which usually means that some scopes get merged, or because of + * splitting, which separates scopes. In either case, nodes with scope references need to be + * adapted, which is done with {@link #replaceAfterScopeChange(ScopeAdaptationVisitor)}. + */ +public interface ScopeReference { + + /** + * Replaces the current node with one that is adapted to match the a changed scope. + * + *

A scope change might have been caused by splitting or inlining. The + * {@link ScopeAdaptationVisitor} provides access to the scope information, to obtain updated + * scope references, e.g., valid variable or frame slot objects, or for convenience also + * complete updated read/write nodes. + * + * @param visitor the {@link ScopeAdaptationVisitor} that manages the scope adaptation and + * provides access to scope information + */ + void replaceAfterScopeChange(ScopeAdaptationVisitor visitor); +} diff --git a/src/bd/inlining/nodes/WithSource.java b/src/bd/inlining/nodes/WithSource.java new file mode 100644 index 0000000..51763cf --- /dev/null +++ b/src/bd/inlining/nodes/WithSource.java @@ -0,0 +1,22 @@ +package bd.inlining.nodes; + +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.source.SourceSection; + + +/** + * All nodes that are handled by inlining are expected to implement {@link WithSource}, which + * is used to make sure they have the source section attribution after inlining. + */ +public interface WithSource { + + /** + * Initialize the node with the source section. + * + * @param the type of node + * + * @param source the source section of the node + * @return the node itself + */ + T initialize(SourceSection source); +} diff --git a/src/bd/tools/nodes/Operation.java b/src/bd/tools/nodes/Operation.java index 4145760..a17b41e 100644 --- a/src/bd/tools/nodes/Operation.java +++ b/src/bd/tools/nodes/Operation.java @@ -8,11 +8,15 @@ public interface Operation { * The name or identifier of an operation implemented by a node. * An addition node could return for instance "+". The name should be * understandable by humans, and might be shown in a user interface. + * + * @return name of the operation */ String getOperation(); /** * The number of arguments on which the operation depends. + * + * @return number of required arguments */ int getNumArguments(); } diff --git a/tests/bd/inlining/InliningTests.java b/tests/bd/inlining/InliningTests.java new file mode 100644 index 0000000..9b28122 --- /dev/null +++ b/tests/bd/inlining/InliningTests.java @@ -0,0 +1,86 @@ +package bd.inlining; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import com.oracle.truffle.api.source.Source; +import com.oracle.truffle.api.source.SourceSection; + +import bd.basic.ProgramDefinitionError; +import bd.testsetup.AddNodeFactory; +import bd.testsetup.ExprNode; +import bd.testsetup.LambdaNode; +import bd.testsetup.StringId; +import bd.testsetup.ValueNode; +import bd.testsetup.ValueSpecializedNode; + + +public class InliningTests { + + private final SourceSection source = + Source.newBuilder("test").name("test").mimeType("x/test").build().createSection(1); + + private final InlinableNodes nodes = + new InlinableNodes(new StringId(), Nodes.getInlinableNodes(), + Nodes.getInlinableFactories()); + + @Test + public void testNonInlinableNode() throws ProgramDefinitionError { + List argNodes = new ArrayList<>(); + argNodes.add(AddNodeFactory.create(null, null)); + assertNull(nodes.inline("value", argNodes, null, null)); + } + + @Test + public void testValueNode() throws ProgramDefinitionError { + List argNodes = new ArrayList<>(); + LambdaNode arg = new LambdaNode(); + argNodes.add(arg); + ExprNode valueNode = nodes.inline("value", argNodes, null, source); + assertNotNull(valueNode); + assertTrue(valueNode instanceof ValueNode); + + ValueNode value = (ValueNode) valueNode; + + assertNotEquals(arg, value.inlined); + assertEquals(arg, value.original); + + assertTrue(value.trueVal); + assertFalse(value.falseVal); + + assertTrue(valueNode.getSourceSection() == source); + } + + @Test + public void testValueSpecNode() throws ProgramDefinitionError { + List argNodes = new ArrayList<>(); + LambdaNode arg = new LambdaNode(); + argNodes.add(arg); + ExprNode valueNode = nodes.inline("valueSpec", argNodes, null, source); + assertNotNull(valueNode); + assertTrue(valueNode instanceof ValueSpecializedNode); + + ValueSpecializedNode value = (ValueSpecializedNode) valueNode; + + assertNotEquals(arg, value.inlined); + assertEquals(arg, value.getLambda()); + assertTrue(valueNode.getSourceSection() == source); + } + + @Test + public void testEmptyInit() throws ProgramDefinitionError { + InlinableNodes n = + new InlinableNodes<>(new StringId(), null, null); + + assertNull(n.inline("nonExisting", null, null, null)); + } +} diff --git a/tests/bd/inlining/Nodes.java b/tests/bd/inlining/Nodes.java new file mode 100644 index 0000000..05c12c6 --- /dev/null +++ b/tests/bd/inlining/Nodes.java @@ -0,0 +1,32 @@ +package bd.inlining; + +import java.util.ArrayList; +import java.util.List; + +import com.oracle.truffle.api.dsl.NodeFactory; +import com.oracle.truffle.api.nodes.Node; + +import bd.testsetup.IfNodeFactory; +import bd.testsetup.ValueNode; +import bd.testsetup.ValueSpecializedNodeFactory; + + +class Nodes { + + protected static List> getInlinableNodes() { + List> nodes = new ArrayList<>(); + + nodes.add(ValueNode.class); + + return nodes; + } + + protected static List> getInlinableFactories() { + List> factories = new ArrayList<>(); + + factories.add(IfNodeFactory.getInstance()); + factories.add(ValueSpecializedNodeFactory.getInstance()); + + return factories; + } +} diff --git a/tests/bd/inlining/TScope.java b/tests/bd/inlining/TScope.java new file mode 100644 index 0000000..2084c61 --- /dev/null +++ b/tests/bd/inlining/TScope.java @@ -0,0 +1,25 @@ +package bd.inlining; + +public class TScope implements Scope { + + @Override + @SuppressWarnings("unchecked") + public Variable[] getVariables() { + return null; + } + + @Override + public TScope getOuterScopeOrNull() { + return null; + } + + @Override + public TScope getScope(final Void method) { + return null; + } + + @Override + public String getName() { + return null; + } +} diff --git a/tests/bd/inlining/TScopeBuilder.java b/tests/bd/inlining/TScopeBuilder.java new file mode 100644 index 0000000..5cf44fb --- /dev/null +++ b/tests/bd/inlining/TScopeBuilder.java @@ -0,0 +1,16 @@ +package bd.inlining; + +import com.oracle.truffle.api.source.SourceSection; + +import bd.basic.ProgramDefinitionError; +import bd.inlining.nodes.Inlinable; + + +public class TScopeBuilder implements ScopeBuilder { + + @Override + public Variable introduceTempForInlinedVersion(final Inlinable node, + final SourceSection source) throws ProgramDefinitionError { + return null; + } +} diff --git a/tests/bd/testsetup/ExprNode.java b/tests/bd/testsetup/ExprNode.java index a9a5f23..e8ab621 100644 --- a/tests/bd/testsetup/ExprNode.java +++ b/tests/bd/testsetup/ExprNode.java @@ -3,9 +3,26 @@ import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.nodes.UnexpectedResultException; +import com.oracle.truffle.api.source.SourceSection; +import bd.inlining.nodes.WithSource; -public abstract class ExprNode extends Node { + +public abstract class ExprNode extends Node implements WithSource { + + private SourceSection sourceSection; + + @Override + @SuppressWarnings("unchecked") + public ExprNode initialize(final SourceSection sourceSection) { + this.sourceSection = sourceSection; + return this; + } + + @Override + public SourceSection getSourceSection() { + return sourceSection; + } public abstract Object executeGeneric(VirtualFrame frame); @@ -17,4 +34,13 @@ public int executeInt(final VirtualFrame frame) throws UnexpectedResultException throw new UnexpectedResultException(result); } } + + public boolean executeBool(final VirtualFrame frame) throws UnexpectedResultException { + Object result = executeGeneric(frame); + if (result instanceof Boolean) { + return (boolean) result; + } else { + throw new UnexpectedResultException(result); + } + } } diff --git a/tests/bd/testsetup/IfNode.java b/tests/bd/testsetup/IfNode.java new file mode 100644 index 0000000..1482f3f --- /dev/null +++ b/tests/bd/testsetup/IfNode.java @@ -0,0 +1,21 @@ +package bd.testsetup; + +import com.oracle.truffle.api.dsl.GenerateNodeFactory; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.dsl.Specialization; + +import bd.inlining.Inline; + + +@NodeChild(value = "cond", type = ExprNode.class) +@NodeChild(value = "thenBranch", type = ExprNode.class) +@NodeChild(value = "elseBranch", type = ExprNode.class) +@Inline(selector = "if", inlineableArgIdx = {1, 2}) +@GenerateNodeFactory +public abstract class IfNode extends ExprNode { + + @Specialization + public Object doIf(final boolean cond, final Object thenBranch, final Object elseBranch) { + return null; + } +} diff --git a/tests/bd/testsetup/LambdaNode.java b/tests/bd/testsetup/LambdaNode.java new file mode 100644 index 0000000..96826b4 --- /dev/null +++ b/tests/bd/testsetup/LambdaNode.java @@ -0,0 +1,20 @@ +package bd.testsetup; + +import com.oracle.truffle.api.frame.VirtualFrame; + +import bd.inlining.TScopeBuilder; +import bd.inlining.nodes.Inlinable; + + +public final class LambdaNode extends ExprNode implements Inlinable { + + @Override + public Object executeGeneric(final VirtualFrame frame) { + return null; + } + + @Override + public ExprNode inline(final TScopeBuilder scopeBuilder) { + return new LambdaNode(); + } +} diff --git a/tests/bd/testsetup/ValueNode.java b/tests/bd/testsetup/ValueNode.java new file mode 100644 index 0000000..419c1d0 --- /dev/null +++ b/tests/bd/testsetup/ValueNode.java @@ -0,0 +1,32 @@ +package bd.testsetup; + +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.frame.VirtualFrame; + +import bd.inlining.Inline; +import bd.inlining.Inline.False; +import bd.inlining.Inline.True; + + +@NodeChild(value = "lambda", type = ExprNode.class) +@Inline(selector = "value", inlineableArgIdx = {0}, additionalArgs = {True.class, False.class}) +public final class ValueNode extends ExprNode { + + public final LambdaNode original; // not a child + public final LambdaNode inlined; // not a child + public final boolean trueVal; + public final boolean falseVal; + + public ValueNode(final LambdaNode original, final LambdaNode inlined, final boolean arg1, + final boolean arg2) { + this.original = original; + this.inlined = inlined; + this.trueVal = arg1; + this.falseVal = arg2; + } + + @Override + public Object executeGeneric(final VirtualFrame frame) { + return null; + } +} diff --git a/tests/bd/testsetup/ValueSpecializedNode.java b/tests/bd/testsetup/ValueSpecializedNode.java new file mode 100644 index 0000000..137d336 --- /dev/null +++ b/tests/bd/testsetup/ValueSpecializedNode.java @@ -0,0 +1,27 @@ +package bd.testsetup; + +import com.oracle.truffle.api.dsl.GenerateNodeFactory; +import com.oracle.truffle.api.dsl.NodeChild; +import com.oracle.truffle.api.dsl.Specialization; + +import bd.inlining.Inline; + + +@NodeChild(value = "lambda", type = ExprNode.class) +@Inline(selector = "valueSpec", inlineableArgIdx = {0}) +@GenerateNodeFactory +public abstract class ValueSpecializedNode extends ExprNode { + + public final LambdaNode inlined; + + public ValueSpecializedNode(final LambdaNode inlined) { + this.inlined = inlined; + } + + public abstract ExprNode getLambda(); + + @Specialization + public int doInt(final int lambda) { + return 42; + } +}