aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java')
-rw-r--r--src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java278
1 files changed, 278 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java b/src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java
index df0c1977ab..cf68aeb6f4 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java
@@ -13,13 +13,54 @@
// limitations under the License.
package com.google.devtools.build.lib.syntax;
+import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.events.Location.LineAndColumn;
import com.google.devtools.build.lib.profiler.Profiler;
import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.syntax.compiler.ByteCodeUtils;
+import com.google.devtools.build.lib.syntax.compiler.DebugInfo;
+import com.google.devtools.build.lib.syntax.compiler.LoopLabels;
+import com.google.devtools.build.lib.syntax.compiler.ReflectionUtils;
+import com.google.devtools.build.lib.syntax.compiler.VariableScope;
import com.google.devtools.build.lib.vfs.PathFragment;
+import net.bytebuddy.ByteBuddy;
+import net.bytebuddy.asm.ClassVisitorWrapper;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.modifier.MethodManifestation;
+import net.bytebuddy.description.modifier.Ownership;
+import net.bytebuddy.description.modifier.TypeManifestation;
+import net.bytebuddy.description.modifier.Visibility;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.dynamic.DynamicType.Unloaded;
+import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
+import net.bytebuddy.implementation.Implementation;
+import net.bytebuddy.implementation.MethodDelegation;
+import net.bytebuddy.implementation.bytecode.ByteCodeAppender;
+import net.bytebuddy.implementation.bytecode.member.MethodReturn;
+import net.bytebuddy.matcher.ElementMatchers;
+
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.util.Textifier;
+import org.objectweb.asm.util.TraceClassVisitor;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
/**
* The actual function registered in the environment. This function is defined in the
* parsed code using {@link FunctionDefStatement}.
@@ -31,12 +72,20 @@ public class UserDefinedFunction extends BaseFunction {
// we close over the globals at the time of definition
private final Environment.Frame definitionGlobals;
+ private Optional<Method> method;
+ // TODO(bazel-team) make this configurable once the compiler is stable
+ public static boolean debugCompiler = false;
+ public static boolean debugCompilerPrintByteCode = false;
+ private static File debugFolder;
+ public static boolean enableCompiler = false;
+
protected UserDefinedFunction(Identifier function,
FunctionSignature.WithValues<Object, SkylarkType> signature,
ImmutableList<Statement> statements, Environment.Frame definitionGlobals) {
super(function.getName(), signature, function.getLocation());
this.statements = statements;
this.definitionGlobals = definitionGlobals;
+ method = enableCompiler ? buildCompiledFunction() : Optional.<Method>absent();
}
public FunctionSignature.WithValues<Object, SkylarkType> getFunctionSignature() {
@@ -59,6 +108,13 @@ public class UserDefinedFunction extends BaseFunction {
getName(), Iterables.getLast(env.getStackTrace()).getName()));
}
+ if (enableCompiler && method.isPresent()) {
+ Object returnValue = callCompiledFunction(arguments, ast, env);
+ if (returnValue != null) {
+ return returnValue;
+ }
+ }
+
Profiler.instance().startTask(ProfilerTask.SKYLARK_USER_FN,
getLocationPathAndLine() + "#" + getName());
try {
@@ -91,6 +147,161 @@ public class UserDefinedFunction extends BaseFunction {
}
}
+ private Object callCompiledFunction(Object[] arguments, FuncallExpression ast, Environment env) {
+ compilerDebug("Calling compiled function " + getLocationPathAndLine() + " " + getName());
+ try {
+ env.enterScope(this, ast, definitionGlobals);
+
+ return method
+ .get()
+ .invoke(null, ImmutableList.builder().add(arguments).add(env).build().toArray());
+
+ } catch (IllegalAccessException e) {
+ // this should never happen
+ throw new RuntimeException(
+ "Compiler created code that could not be accessed reflectively.", e);
+ } catch (InvocationTargetException e) {
+ compilerDebug("Error running compiled version", e.getCause());
+ return null;
+ } finally {
+ env.exitScope();
+ }
+ }
+
+ /**
+ * Generates a subclass of {@link CompiledFunction} with a static method "call" and static
+ * methods for getting information from a {@link DebugInfo} instance.
+ *
+ * <p>The "call" method contains the compiled version of this function's AST.
+ */
+ private Optional<Method> buildCompiledFunction() {
+ // replace the / character in the path so we have file system compatible class names
+ // the java specification mentions that $ should be used in generated code
+ // see http://docs.oracle.com/javase/specs/jls/se7/html/jls-3.html#jls-3.8
+ String path =
+ location.getPath() != null ? location.getPath().getPathString().replace('/', '$') : "";
+ String compiledFunctionClassName =
+ CompiledFunction.class.getCanonicalName() + path + "$" + getName();
+ compilerDebug("Compiling " + getLocationPathAndLine() + " " + getName());
+ try {
+ int publicStatic = Visibility.PUBLIC.getMask() | Ownership.STATIC.getMask();
+ TypeDescription.Latent latentCompiledFunctionClass =
+ new TypeDescription.Latent(
+ compiledFunctionClassName,
+ publicStatic | TypeManifestation.FINAL.getMask(),
+ new TypeDescription.ForLoadedType(CompiledFunction.class),
+ Collections.<TypeDescription>emptyList());
+ MethodDescription getAstNode =
+ new MethodDescription.Latent(
+ latentCompiledFunctionClass,
+ new MethodDescription.Token(
+ "getAstNode",
+ publicStatic | MethodManifestation.FINAL.getMask(),
+ new TypeDescription.ForLoadedType(ASTNode.class),
+ Arrays.asList(new TypeDescription.ForLoadedType(int.class))));
+ MethodDescription getLocation =
+ new MethodDescription.Latent(
+ latentCompiledFunctionClass,
+ new MethodDescription.Token(
+ "getLocation",
+ publicStatic | MethodManifestation.FINAL.getMask(),
+ new TypeDescription.ForLoadedType(Location.class),
+ Arrays.asList(new TypeDescription.ForLoadedType(int.class))));
+
+ DebugInfo debugInfo = new DebugInfo(getAstNode, getLocation);
+ FunctionSignature sig = signature.getSignature();
+ VariableScope scope = VariableScope.function(sig.getNames());
+ Implementation compiledImplementation = compileBody(scope, debugInfo);
+
+ List<Class<?>> parameterTypes = sig.getShape().toClasses();
+ parameterTypes.add(Environment.class);
+ Unloaded<CompiledFunction> unloadedImplementation =
+ new ByteBuddy()
+ .withClassVisitor(new StackMapFrameClassVisitor(debugCompilerPrintByteCode))
+ .subclass(CompiledFunction.class)
+ .name(compiledFunctionClassName)
+ .defineMethod(
+ "call",
+ Object.class,
+ parameterTypes,
+ Visibility.PUBLIC,
+ Ownership.STATIC,
+ MethodManifestation.FINAL)
+ .intercept(compiledImplementation)
+ .defineMethod(getAstNode)
+ // TODO(bazel-team) unify the two delegate fields into one, probably needs a custom
+ // ImplementationDelegate that adds it only once? or just create the static field
+ // itself with the correct value and create getAstNode & getLocation with a custom
+ // implementation using it
+ .intercept(
+ MethodDelegation.to(debugInfo, DebugInfo.class, "getAstNodeDelegate")
+ .filter(ElementMatchers.named("getAstNode")))
+ .defineMethod(getLocation)
+ .intercept(
+ MethodDelegation.to(debugInfo, DebugInfo.class, "getLocationDelegate")
+ .filter(ElementMatchers.named("getLocation")))
+ .make();
+ saveByteCode(unloadedImplementation);
+ Class<? extends CompiledFunction> functionClass =
+ unloadedImplementation
+ .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
+ .getLoaded();
+
+ return Optional.of(
+ ReflectionUtils.getMethod(
+ functionClass,
+ "call",
+ parameterTypes.toArray(new Class<?>[parameterTypes.size()]))
+ .getLoadedMethod());
+ } catch (Throwable e) {
+ compilerDebug("Error while compiling", e);
+ // TODO(bazel-team) don't capture all throwables? couldn't compile this, log somewhere?
+ return Optional.absent();
+ }
+ }
+
+ /**
+ * Saves byte code to a temporary directory prefixed with "skylarkbytecode" in the system
+ * default temporary directory.
+ */
+ private void saveByteCode(Unloaded<CompiledFunction> unloadedImplementation) {
+ if (debugCompiler) {
+ try {
+ if (debugFolder == null) {
+ debugFolder = Files.createTempDirectory("skylarkbytecode").toFile();
+ }
+ unloadedImplementation.saveIn(debugFolder);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /**
+ * Builds a byte code implementation of the AST.
+ */
+ private Implementation compileBody(VariableScope scope, DebugInfo debugInfo) {
+ List<ByteCodeAppender> code = new ArrayList<>(statements.size());
+ code.add(null); // reserve space for later addition of the local variable initializer
+
+ for (Statement statement : statements) {
+ code.add(statement.compile(scope, LoopLabels.ABSENT, debugInfo));
+ }
+ // add a return None if there are no statements or the last one to ensure the method always
+ // returns something. This implements the interpreters behavior.
+ if (statements.isEmpty()
+ || !(statements.get(statements.size() - 1) instanceof ReturnStatement)) {
+ code.add(new ByteCodeAppender.Simple(Runtime.GET_NONE, MethodReturn.REFERENCE));
+ }
+ // we now know which variables we used in the method, so assign them "undefined" (i.e. null)
+ // at the beginning of the method
+ code.set(0, scope.createLocalVariablesUndefined());
+ // TODO(bazel-team) wrap ByteCodeAppender in our own type including a reference to the ASTNode
+ // it came from and verify the stack and local variables ourselves, because ASM does not help
+ // with debugging much when its stack map frame calculation fails because of invalid byte code
+ return new Implementation.Simple(ByteCodeUtils.compoundAppender(code));
+ }
+
/**
* Returns the location (filename:line) of the BaseFunction's definition.
*
@@ -113,4 +324,71 @@ public class UserDefinedFunction extends BaseFunction {
}
return builder.toString();
}
+
+ private void compilerDebug(String message) {
+ System.err.println(message);
+ }
+
+ private void compilerDebug(String message, Throwable e) {
+ compilerDebug(message);
+ e.printStackTrace();
+ }
+
+ /**
+ * A simple super class for all compiled function's classes.
+ */
+ protected abstract static class CompiledFunction {}
+
+ /**
+ * A {@link Textifier} for printing the generated byte code that keeps the ASM-internal label
+ * names in place for easier debugging with IDE debuggers.
+ */
+ private static class DebugTextifier extends Textifier {
+ DebugTextifier() {
+ super(Opcodes.ASM5);
+ }
+
+ @Override
+ protected void appendLabel(Label l) {
+ buf.append(l.toString());
+ }
+
+ @Override
+ protected Textifier createTextifier() {
+ return new DebugTextifier();
+ }
+ }
+
+ /**
+ * Passes the {@link ClassWriter#COMPUTE_FRAMES} hint to ASM and optionally prints generated
+ * byte code to System.err.
+ */
+ private static class StackMapFrameClassVisitor implements ClassVisitorWrapper {
+
+ private final boolean debug;
+
+ private StackMapFrameClassVisitor(boolean debug) {
+ this.debug = debug;
+ }
+
+ @Override
+ public int mergeWriter(int hint) {
+ return hint | ClassWriter.COMPUTE_FRAMES;
+ }
+
+ @Override
+ public int mergeReader(int hint) {
+ return hint;
+ }
+
+ @Override
+ public ClassVisitor wrap(ClassVisitor classVisitor) {
+ if (debug) {
+ return new TraceClassVisitor(
+ classVisitor, new DebugTextifier(), new PrintWriter(System.err, true));
+ } else {
+ return classVisitor;
+ }
+ }
+ }
}